├── .gitignore ├── .hound.yml ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── ci ├── Dockerfile └── microscope.yaml ├── docker ├── motd └── profile ├── docs ├── logo.svg ├── microscope.yaml └── microscope1.0.1.yaml ├── microscope ├── __init__.py ├── __main__.py ├── batch │ ├── __init__.py │ └── batch.py ├── monitor │ ├── __init__.py │ ├── epresolver.py │ ├── monitor.py │ ├── parser.py │ ├── runner.py │ └── test_monitor.py └── ui │ ├── __init__.py │ └── ui.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | microscope.egg-info 4 | 5 | cilium-* 6 | pyz 7 | 8 | *__pycache__ 9 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | fail_on_violations: true 2 | flake8: 3 | enabled: true 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.0-alpine3.8 2 | 3 | RUN apk update 4 | RUN apk add make libffi-dev build-base libressl-dev 5 | WORKDIR /usr/src/microscope 6 | COPY . . 7 | RUN make pyz 8 | 9 | FROM python:3.7.0-alpine3.8 10 | 11 | COPY docker/motd /etc/motd 12 | COPY docker/profile /root/profile 13 | ENV ENV=/root/profile 14 | 15 | COPY --from=0 /usr/src/microscope/pyz/microscope.pyz /bin/microscope 16 | RUN ln -s /bin/microscope /bin/cilium-microscope 17 | 18 | WORKDIR /usr/src/microscope 19 | 20 | CMD [ "microscope" ] 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | distpath = pyz/microscope 2 | pyzpath = pyz/microscope.pyz 3 | 4 | pyz: 5 | mkdir -p $(distpath) 6 | pip install -r requirements.txt -t $(distpath) 7 | cp -r microscope/ $(distpath) 8 | cp microscope/__main__.py $(distpath)/__main__.py 9 | python -m zipapp $(distpath) 10 | #add shebang 11 | echo '#!/usr/bin/env python' | cat - $(pyzpath) > pyz/tmp 12 | mv pyz/tmp $(pyzpath) 13 | chmod +x $(pyzpath) 14 | 15 | clean: 16 | rm -rf pyz 17 | rm -rf dist 18 | 19 | docker-image: 20 | docker build -t cilium/microscope . 21 | 22 | docker-ci: 23 | docker build -t cilium/microscope:ci ci 24 | 25 | install: 26 | python setup.py install 27 | 28 | dist: 29 | python setup.py sdist 30 | 31 | upload: dist 32 | twine upload dist/* 33 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |logo| 2 | 3 | Cilium Microscope 4 | ================= 5 | 6 | Cilium microscope allows you to see ``cilium monitor`` output from all your cilium nodes. 7 | This allows you to have one simple to use command to interact with your cilium nodes 8 | within k8s cluster. 9 | 10 | 11 | Running microscope in your Kubernetes cluster 12 | --------------------------------------------- 13 | 14 | ``kubectl create -f https://raw.githubusercontent.com/cilium/microscope/master/docs/microscope.yaml`` will create a pod in your kube-system namespace to which you can connect to run ``microscope`` with ``kubectl exec -it -n kube-system microscope sh``. This will also create RBAC objects which ``microscope`` needs in order to do its work. 15 | 16 | Alternatively, you can use ``kubectl run -i --tty microscope --image cilium/microscope --restart=Never -- sh``. This won't work if you have RBAC enabled in your cluster. 17 | 18 | In any case you will end up with a shell inside ``microscope`` pod. 19 | ``microscope -h`` inside this shell will show ``microscope`` help. 20 | 21 | 22 | Running microscope locally 23 | -------------------------- 24 | 25 | To run ``microscope`` locally, you need to have Python 3.5 or newer installed. Using virtualenv is recommended, but not necessary. 26 | 27 | ``microscope`` is available as a package in PyPI, so all you need to do is run ``pip install cilium-microscope``. ``microscope`` executable should be available in your path. 28 | 29 | Alternatively you can run ``make`` to build self-contained Python archive which will container all dependencies and requires only Python to run. 30 | 31 | The archive will be located in ``dist/microscope.pyz``, and should be executable directly. 32 | 33 | 34 | .. |logo| image:: https://cdn.rawgit.com/cilium/microscope/master/docs/logo.svg 35 | :alt: Cilium Microscope Logo 36 | -------------------------------------------------------------------------------- /ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cilium/microscope 2 | 3 | RUN apk --no-cache add --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing moreutils 4 | 5 | CMD [ "microscope" ] 6 | -------------------------------------------------------------------------------- /ci/microscope.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | metadata: 4 | name: microscope 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: microscope 9 | subjects: 10 | - kind: ServiceAccount 11 | name: microscope 12 | namespace: kube-system 13 | --- 14 | kind: ClusterRole 15 | apiVersion: rbac.authorization.k8s.io/v1beta1 16 | metadata: 17 | name: microscope 18 | rules: 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - pods 23 | - namespaces 24 | - nodes 25 | verbs: 26 | - get 27 | - list 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - pods/exec 32 | verbs: 33 | - create 34 | - get 35 | - apiGroups: 36 | - cilium.io 37 | resources: 38 | - ciliumnetworkpolicies 39 | - ciliumendpoints 40 | verbs: 41 | - get 42 | - list 43 | - watch 44 | - apiGroups: 45 | - networking.k8s.io 46 | resources: 47 | - networkpolicies 48 | verbs: 49 | - get 50 | - list 51 | - watch 52 | --- 53 | apiVersion: v1 54 | kind: ServiceAccount 55 | metadata: 56 | name: microscope 57 | namespace: kube-system 58 | --- 59 | apiVersion: v1 60 | kind: Pod 61 | metadata: 62 | name: microscope 63 | namespace: kube-system 64 | labels: 65 | k8s-app: microscope 66 | spec: 67 | serviceAccountName: microscope 68 | containers: 69 | - args: 70 | - sleep 71 | - "100000" 72 | image: docker.io/cilium/microscope:ci 73 | imagePullPolicy: IfNotPresent 74 | name: microscope 75 | -------------------------------------------------------------------------------- /docker/motd: -------------------------------------------------------------------------------- 1 | _ 2 | (_) 3 | _ __ ___ _ ___ _ __ ___ ___ ___ ___ _ __ ___ 4 | | '_ ` _ \| |/ __| '__/ _ \/ __|/ __/ _ \| '_ \ / _ \ 5 | | | | | | | | (__| | | (_) \__ \ (_| (_) | |_) | __/ 6 | |_| |_| |_|_|\___|_| \___/|___/\___\___/| .__/ \___| 7 | | | 8 | |_| 9 | example usage: 10 | 11 | microscope --pod pod_name # shows all monitor events related to your pod in default namespace 12 | 13 | microscope --to-pod isolated:secret_pod # shows all packets going to pod in `isolated` namespace 14 | 15 | microscope --from-selector k8s-app=your-app # shows all packets sent by all pods with `k8s-app=your-app` label 16 | 17 | microscope --node cilium-xxxx # only show output from particular cilium node specified by its pod name 18 | 19 | full help: 20 | microscope -h 21 | -------------------------------------------------------------------------------- /docker/profile: -------------------------------------------------------------------------------- 1 | sleep 1 2 | cat /etc/motd 3 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /docs/microscope.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | metadata: 4 | name: microscope 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: microscope 9 | subjects: 10 | - kind: ServiceAccount 11 | name: microscope 12 | namespace: kube-system 13 | --- 14 | kind: ClusterRole 15 | apiVersion: rbac.authorization.k8s.io/v1beta1 16 | metadata: 17 | name: microscope 18 | rules: 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - pods 23 | - namespaces 24 | - nodes 25 | verbs: 26 | - get 27 | - list 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - pods/exec 32 | verbs: 33 | - create 34 | - get 35 | - apiGroups: 36 | - cilium.io 37 | resources: 38 | - ciliumnetworkpolicies 39 | - ciliumendpoints 40 | verbs: 41 | - get 42 | - list 43 | - watch 44 | - apiGroups: 45 | - networking.k8s.io 46 | resources: 47 | - networkpolicies 48 | verbs: 49 | - get 50 | - list 51 | - watch 52 | --- 53 | apiVersion: v1 54 | kind: ServiceAccount 55 | metadata: 56 | name: microscope 57 | namespace: kube-system 58 | --- 59 | apiVersion: v1 60 | kind: Pod 61 | metadata: 62 | name: microscope 63 | namespace: kube-system 64 | labels: 65 | k8s-app: microscope 66 | spec: 67 | serviceAccountName: microscope 68 | containers: 69 | - args: 70 | - sleep 71 | - "100000" 72 | image: docker.io/cilium/microscope:1.1.0 73 | imagePullPolicy: IfNotPresent 74 | name: microscope 75 | -------------------------------------------------------------------------------- /docs/microscope1.0.1.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | metadata: 4 | name: microscope 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: microscope 9 | subjects: 10 | - kind: ServiceAccount 11 | name: microscope 12 | namespace: kube-system 13 | --- 14 | kind: ClusterRole 15 | apiVersion: rbac.authorization.k8s.io/v1beta1 16 | metadata: 17 | name: microscope 18 | rules: 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - pods 23 | - namespaces 24 | - nodes 25 | verbs: 26 | - get 27 | - list 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - pods/exec 32 | verbs: 33 | - create 34 | - get 35 | - apiGroups: 36 | - cilium.io 37 | resources: 38 | - ciliumnetworkpolicies 39 | - ciliumendpoints 40 | verbs: 41 | - get 42 | - list 43 | - watch 44 | - apiGroups: 45 | - networking.k8s.io 46 | resources: 47 | - networkpolicies 48 | verbs: 49 | - get 50 | - list 51 | - watch 52 | --- 53 | apiVersion: v1 54 | kind: ServiceAccount 55 | metadata: 56 | name: microscope 57 | namespace: kube-system 58 | --- 59 | apiVersion: v1 60 | kind: Pod 61 | metadata: 62 | name: microscope 63 | namespace: kube-system 64 | labels: 65 | k8s-app: microscope 66 | spec: 67 | serviceAccountName: microscope 68 | containers: 69 | - args: 70 | - sleep 71 | - "100000" 72 | image: docker.io/cilium/microscope:1.0.1 73 | imagePullPolicy: IfNotPresent 74 | name: microscope 75 | -------------------------------------------------------------------------------- /microscope/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cilium/microscope/db922b79fb28e500f9a2d1f749620485cfda9dc0/microscope/__init__.py -------------------------------------------------------------------------------- /microscope/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import signal 3 | 4 | from kubernetes import config 5 | from kubernetes.client import Configuration 6 | from kubernetes.client.apis import core_v1_api 7 | 8 | from microscope.monitor.runner import MonitorRunner, MonitorArgs 9 | from microscope.monitor.runner import NoEndpointException 10 | from microscope.ui.ui import ui 11 | from microscope.batch.batch import batch 12 | 13 | 14 | def main(): 15 | parser = argparse.ArgumentParser() 16 | 17 | parser.add_argument('--timeout-monitors', type=int, default=0, 18 | help='Will remove monitor output which did ' 19 | 'not update in last `timeout` seconds. ' 20 | 'Will not work on last monitor on screen.') 21 | parser.add_argument('--verbose', action='store_true', default=False) 22 | parser.add_argument('--hex', action='store_true', default=False) 23 | 24 | # taken from github.com/cilium/cilium/cmd/monitor.go 25 | type_choices = ['drop', 'debug', 'capture', 'trace', 'l7', 'agent'] 26 | parser.add_argument('--type', action='append', default=[], 27 | choices=type_choices) 28 | 29 | parser.add_argument('--node', action='append', default=[], 30 | help='Specify which nodes monitor will be run on. ' 31 | 'Can match either by cilium pod names or k8s node ' 32 | 'names. Can specify multiple.') 33 | 34 | parser.add_argument('--selector', action='append', default=[], 35 | help='k8s equality label selectors for pods which ' 36 | 'monitor should listen to. each selector will ' 37 | 'retrieve its own set of pods. ' 38 | 'Format is "label-name=label-value" ' 39 | 'Can specify multiple.') 40 | parser.add_argument('--pod', action='append', default=[], 41 | help='pod names in form of "namespace:pod-name", ' 42 | 'if there is no namespace, default is assumed. ' 43 | 'Can specify multiple.') 44 | parser.add_argument('--endpoint', action='append', type=int, default=[], 45 | help='Cilium endpoint ids. Can specify multiple.') 46 | parser.add_argument('--ip', action='append', default=[], 47 | help='K8s pod ips. Can specify multiple.') 48 | 49 | parser.add_argument('--to-selector', action='append', default=[], 50 | help='k8s equality label selectors for pods which ' 51 | 'monitor should listen to. each selector will ' 52 | 'retrieve its own set of pods. ' 53 | 'Matches events that go to selected pods. ' 54 | 'Format is "label-name=label-value" ' 55 | 'Can specify multiple.') 56 | parser.add_argument('--to-pod', action='append', default=[], 57 | help='pod names in form of "namespace:pod-name", ' 58 | 'if there is no namespace, default is assumed. ' 59 | 'Matches events that go to specified pods. ' 60 | 'Can specify multiple.') 61 | parser.add_argument('--to-endpoint', action='append', type=int, default=[], 62 | help='Cilium endpoint ids. ' 63 | 'Matches events that go to specified endpoints. ' 64 | 'Can specify multiple.') 65 | parser.add_argument('--to-ip', action='append', default=[], 66 | help='K8s pod ips. Can specify multiple.') 67 | 68 | parser.add_argument('--from-selector', action='append', default=[], 69 | help='k8s equality label selectors for pods which ' 70 | 'monitor should listen to. each selector will ' 71 | 'retrieve its own set of pods. ' 72 | 'Matches events that come from selected pods. ' 73 | 'Format is "label-name=label-value" ' 74 | 'Can specify multiple.') 75 | parser.add_argument('--from-pod', action='append', default=[], 76 | help='pod names in form of "namespace:pod-name", ' 77 | 'if there is no namespace, default is assumed. ' 78 | 'Matches events that come from specified pods. ' 79 | 'Can specify multiple.') 80 | parser.add_argument('--from-endpoint', action='append', type=int, 81 | default=[], 82 | help='Cilium endpoint ids. ' 83 | 'Matches events that come from specified endpoints. ' 84 | 'Can specify multiple.') 85 | parser.add_argument('--from-ip', action='append', default=[], 86 | help='K8s pod ips. Can specify multiple.') 87 | 88 | parser.add_argument('--send-command', type=str, default="", 89 | help='Execute command as-provided in argument on ' 90 | 'all specified nodes and show output.') 91 | 92 | parser.add_argument('--cilium-namespace', type=str, default="kube-system", 93 | help='Specify namespace in which Cilium pods reside') 94 | 95 | parser.add_argument('--clear-monitors', action='store_true', default=False, 96 | help='Kill all `cilium monitor` on Cilium nodes. ' 97 | 'Helpful for debugging') 98 | 99 | parser.add_argument('--rich', action='store_true', default=False, 100 | help='Opens rich ui version') 101 | 102 | parser.add_argument('-n', '--namespace', type=str, default='default', 103 | help='Namespace to look for selected endpoints in') 104 | 105 | parser.add_argument('--raw', action='store_true', default=False, 106 | help='Print out raw monitor output without parsing') 107 | 108 | args = parser.parse_args() 109 | 110 | try: 111 | config.load_kube_config() 112 | except FileNotFoundError: 113 | config.load_incluster_config() 114 | 115 | c = Configuration() 116 | c.assert_hostname = False 117 | Configuration.set_default(c) 118 | api = core_v1_api.CoreV1Api() 119 | runner = MonitorRunner(args.cilium_namespace, api, args.namespace) 120 | 121 | monitor_args = MonitorArgs(args.verbose, args.hex, 122 | args.selector, args.pod, args.endpoint, 123 | args.to_selector, args.to_pod, 124 | args.to_endpoint, args.from_selector, 125 | args.from_pod, args.from_endpoint, args.type, 126 | args.namespace, args.raw, 127 | args.ip, args.to_ip, args.from_ip) 128 | 129 | def handle_signals(_, __): 130 | runner.finish() 131 | 132 | try: 133 | if args.clear_monitors: 134 | cmd = "pkill -f \"cilium monitor\"" 135 | else: 136 | cmd = args.send_command 137 | 138 | runner.run(monitor_args, args.node, cmd) 139 | signal.signal(signal.SIGHUP, handle_signals) 140 | if args.rich: 141 | ui(runner, args.timeout_monitors) 142 | elif not args.clear_monitors: 143 | batch(runner, args.timeout_monitors) 144 | except KeyboardInterrupt: 145 | pass 146 | except NoEndpointException: 147 | print("Cilium endpoints matching pod names/label selectors not found.") 148 | except Exception as e: 149 | print("Exception encountered: " + repr(e) + " stack trace below") 150 | raise e 151 | finally: 152 | runner.finish() 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /microscope/batch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cilium/microscope/db922b79fb28e500f9a2d1f749620485cfda9dc0/microscope/batch/__init__.py -------------------------------------------------------------------------------- /microscope/batch/batch.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | from multiprocessing import Queue 4 | import queue as queuemodule 5 | import typing.io 6 | 7 | from microscope.monitor.runner import MonitorRunner 8 | 9 | 10 | def batch(runner: MonitorRunner, timeout: int): 11 | start_time = time.time() 12 | while(runner.is_alive() and runner.close_queue.empty() 13 | and (start_time + timeout > time.time() or timeout == 0)): 14 | drain_and_print(runner.data_queue, sys.stdout) 15 | 16 | # drain queue 17 | drain_and_print(runner.data_queue, sys.stdout) 18 | 19 | 20 | def drain_and_print(queue: Queue, stream: typing.io): 21 | while True: 22 | try: 23 | output = queue.get(True, 1) 24 | if ("output" in output): 25 | print(f"\n{output['node_name']}: {output['output']}", 26 | end="", file=stream) 27 | stream.flush() 28 | except queuemodule.Empty: 29 | break 30 | -------------------------------------------------------------------------------- /microscope/monitor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cilium/microscope/db922b79fb28e500f9a2d1f749620485cfda9dc0/microscope/monitor/__init__.py -------------------------------------------------------------------------------- /microscope/monitor/epresolver.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Set, Callable 2 | 3 | 4 | # https://github.com/cilium/cilium/blob/master/pkg/identity/numericidentity.go#L33 5 | reserved_identities = { 6 | 0: ["reserved:unknown"], 7 | 1: ["reserved:host"], 8 | 2: ["reserved:world"], 9 | 3: ["reserved:cluster"], 10 | 4: ["reserved:health"], 11 | 5: ["reserved:init"] 12 | } 13 | 14 | def get_pod_name(ep): 15 | try: 16 | podname = ep['status']['external-identifiers']['pod-name'] 17 | except KeyError: 18 | podname = ep['external-identifiers']['pod-name'] 19 | 20 | return podname 21 | 22 | class EndpointResolver: 23 | """EndpointResolver resolves various fields to the pod-name 24 | 25 | endpoint_data: a list of lists of endpoint objects obtained from 26 | cilium-agent or k8s CEPs 27 | """ 28 | def __init__(self, 29 | endpoint_data: [Dict]): 30 | 31 | self.ip_resolutions = {} 32 | self.epid_resolutions = {} 33 | self.ip_to_epid_resolutions = {} 34 | self.endpoint_data = endpoint_data 35 | 36 | for ep in endpoint_data: 37 | podname = get_pod_name(ep) 38 | for ip in ep['status']['networking']['addressing']: 39 | try: 40 | ipv4 = ip['ipv4'] 41 | self.ip_resolutions[ipv4] = podname 42 | self.ip_to_epid_resolutions[ipv4] = ep['id'] 43 | except KeyError: 44 | pass 45 | try: 46 | ipv6 = ip['ipv6'] 47 | self.ip_resolutions[ipv6] = podname 48 | self.ip_to_epid_resolutions[ipv6] = ep['id'] 49 | except KeyError: 50 | pass 51 | 52 | # the str(ep['id']) below is needed because the ID is an 53 | # int in json 54 | self.epid_resolutions[str(ep['id'])] = podname 55 | 56 | ep_identities = {id["id"]: id["labels"] for id in 57 | [e["status"]["identity"] 58 | for e in endpoint_data]} 59 | 60 | self.identities = {**ep_identities, **reserved_identities} 61 | 62 | def resolve_ip(self, ip) -> str: 63 | if ip in self.ip_resolutions: 64 | return self.ip_resolutions[ip] 65 | return "" 66 | 67 | def resolve_eid(self, eid) -> str: 68 | if eid in self.epid_resolutions: 69 | return self.epid_resolutions[eid] 70 | return "" 71 | 72 | def resolve_identity(self, id) -> List: 73 | if id in self.identities: 74 | return self.identities[id] 75 | return "" 76 | 77 | def resolve_id_from_ip(self, ip) -> str: 78 | if ip in self.ip_to_epid_resolutions: 79 | return self.ip_to_epid_resolutions[ip] 80 | return "" 81 | 82 | def resolve_endpoint_ids(self, selectors: List[str], 83 | pod_names: List[str], 84 | ips: List[str], 85 | namespace: str) -> Set[int]: 86 | """resolve_endpoint_ids returns endpoint ids that match 87 | selectors, pod names and ips provided 88 | """ 89 | ids = set() 90 | ids.update( 91 | self.resolve_endpoint_ids_from_pods(pod_names), 92 | self.resolve_endpoint_ids_from_selectors(selectors, namespace), 93 | self.resolve_endpoint_ids_from_ips(ips) 94 | ) 95 | return ids 96 | 97 | def resolve_endpoint_ids_from_pods(self, pod_names: List[str]): 98 | 99 | try: 100 | namesMatch = { 101 | endpoint['id'] for endpoint in self.endpoint_data 102 | if 103 | get_pod_name(endpoint) 104 | in pod_names 105 | } 106 | except (KeyError, TypeError): 107 | # fall back to older API structure 108 | namesMatch = {endpoint['id'] for endpoint in self.endpoint_data 109 | if endpoint['pod-name'] in pod_names} 110 | return namesMatch 111 | 112 | def resolve_endpoint_ids_from_selectors(self, selectors: List[str], 113 | namespace: str): 114 | 115 | namespace_matcher = f"k8s:io.kubernetes.pod.namespace={namespace}" 116 | 117 | def labels_match(data, selectors: List[str], 118 | labels_getter: Callable[[Dict], List[str]]): 119 | return { 120 | endpoint['id'] for endpoint in data 121 | if any([ 122 | any( 123 | [selector in label 124 | for selector in selectors]) 125 | for label 126 | in labels_getter(endpoint) 127 | ]) and namespace_matcher in labels_getter(endpoint) 128 | } 129 | 130 | getters = [ 131 | lambda x: x['status']['labels']['security-relevant'], 132 | lambda x: x['labels']['orchestration-identity'], 133 | lambda x: x['labels']['security-relevant'] 134 | ] 135 | labelsMatch = [] 136 | for getter in getters: 137 | try: 138 | labelsMatch = labels_match( 139 | self.endpoint_data, selectors, getter) 140 | except (KeyError, TypeError): 141 | continue 142 | break 143 | return labelsMatch 144 | 145 | def resolve_endpoint_ids_from_ips(self, ips: List[str]): 146 | return {self.resolve_id_from_ip(ip) for ip in ips} - {''} 147 | -------------------------------------------------------------------------------- /microscope/monitor/monitor.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import signal 3 | import threading 4 | from multiprocessing import Process, Queue 5 | 6 | from kubernetes.client.apis import core_v1_api 7 | from kubernetes.client.rest import ApiException 8 | from kubernetes.stream import stream 9 | 10 | from microscope.monitor.parser import MonitorOutputProcessorVerbose 11 | from microscope.monitor.parser import MonitorOutputProcessorJSON 12 | from microscope.monitor.parser import MonitorOutputProcessorSimple 13 | 14 | 15 | # we are ignoring sigint in monitor processes as they are closed via queue 16 | def sigint_in_monitor(signum, frame): 17 | pass 18 | 19 | 20 | class Monitor: 21 | def __init__(self, 22 | pod_name: str, 23 | node_name: str, 24 | namespace: str, 25 | queue: Queue, 26 | close_queue: Queue, 27 | api: core_v1_api.CoreV1Api, 28 | cmd: List[str], 29 | mode: str, 30 | resolver 31 | ): 32 | self.pod_name = pod_name 33 | self.node_name = node_name 34 | self.namespace = namespace 35 | self.queue = queue 36 | self.close_queue = close_queue 37 | self.api = api 38 | self.cmd = cmd 39 | self.mode = mode 40 | self.resolver = resolver 41 | 42 | self.process = Process(target=self.connect) 43 | self.output = node_name + "\n" 44 | self.output_lock = threading.Semaphore() 45 | 46 | def connect(self): 47 | try: 48 | resp = self.api.read_namespaced_pod(name=self.pod_name, 49 | namespace=self.namespace) 50 | except ApiException as e: 51 | if e.status != 404: 52 | print('Unknown error: %s' % e) 53 | exit(1) 54 | 55 | # calling exec and wait for response. 56 | 57 | resp = stream(self.api.connect_get_namespaced_pod_exec, self.pod_name, 58 | self.namespace, 59 | command=["bash", "-c", " ".join(self.cmd)], 60 | stderr=True, stdin=True, 61 | stdout=True, tty=True, 62 | _preload_content=False) 63 | 64 | signal.signal(signal.SIGINT, sigint_in_monitor) 65 | 66 | if self.mode == "": 67 | processor = MonitorOutputProcessorJSON(self.resolver) 68 | elif self.mode == "raw": 69 | processor = MonitorOutputProcessorSimple() 70 | else: 71 | processor = MonitorOutputProcessorVerbose() 72 | 73 | while resp.is_open(): 74 | for msg in processor: 75 | if msg: 76 | self.queue.put({ 77 | 'name': self.pod_name, 78 | 'node_name': self.node_name, 79 | 'output': msg}) 80 | 81 | resp.update(timeout=1) 82 | if not self.close_queue.empty(): 83 | print("Closing monitor") 84 | resp.write_stdin('\x03') 85 | break 86 | if resp.peek_stdout(): 87 | processor.add_out(resp.read_stdout()) 88 | if resp.peek_stderr(): 89 | processor.add_err(resp.read_stderr()) 90 | 91 | for msg in processor: 92 | if msg: 93 | self.queue.put({ 94 | 'name': self.pod_name, 95 | 'node_name': self.node_name, 96 | 'output': msg}) 97 | 98 | resp.close() 99 | 100 | self.close_queue.cancel_join_thread() 101 | self.queue.close() 102 | self.queue.join_thread() 103 | -------------------------------------------------------------------------------- /microscope/monitor/parser.py: -------------------------------------------------------------------------------- 1 | import queue as queuemodule 2 | import time 3 | import json 4 | from typing import List, Dict, Tuple 5 | 6 | 7 | class MonitorOutputProcessorSimple: 8 | def __init__(self): 9 | self.std_output = queuemodule.Queue() 10 | self.std_err = queuemodule.Queue() 11 | 12 | def add_out(self, out: str): 13 | for line in out.split("\n"): 14 | self.std_output.put(line) 15 | 16 | def add_err(self, err: str): 17 | for line in err.split("\n"): 18 | self.std_err.put(line) 19 | 20 | def get_err(self) -> str: 21 | err = [] 22 | while not self.std_err.empty(): 23 | line = self.std_err.get() 24 | err.append(line) 25 | if err: 26 | return "\n".join(err) 27 | 28 | def __iter__(self): 29 | return self 30 | 31 | def __next__(self) -> str: 32 | err = self.get_err() 33 | if err: 34 | return err 35 | 36 | try: 37 | return self.std_output.get_nowait() 38 | except queuemodule.Empty: 39 | raise StopIteration 40 | 41 | raise StopIteration 42 | 43 | 44 | class MonitorOutputProcessorVerbose(MonitorOutputProcessorSimple): 45 | def __init__(self): 46 | self.std_output = queuemodule.Queue() 47 | self.std_err = queuemodule.Queue() 48 | self.current_msg = [] 49 | self.last_event_wait_timeout = 1500 50 | self.last_event_time = 0 51 | 52 | def __next__(self) -> str: 53 | err = self.get_err() 54 | if err: 55 | return err 56 | 57 | prev_event = self.last_event_time 58 | while not self.std_output.empty(): 59 | line = self.std_output.get() 60 | 61 | self.last_event_time = int(round(time.time() * 1000)) 62 | 63 | if '---' in line: 64 | return self.pop_current(line) 65 | else: 66 | self.current_msg.append(line) 67 | 68 | now = int(round(time.time() * 1000)) 69 | if prev_event + self.last_event_wait_timeout < now: 70 | if self.current_msg: 71 | return self.pop_current() 72 | 73 | raise StopIteration 74 | 75 | def pop_current(self, init: str = "") -> str: 76 | tmp = "\n".join(self.current_msg) 77 | if init: 78 | self.current_msg = [init] 79 | else: 80 | self.current_msg = [] 81 | return tmp 82 | 83 | 84 | class MonitorOutputProcessorJSON(MonitorOutputProcessorSimple): 85 | def __init__(self, resolver): 86 | self.std_output = "" 87 | self.std_err = queuemodule.Queue() 88 | self.resolver = resolver 89 | 90 | def add_out(self, out: str): 91 | self.std_output += out 92 | 93 | def get_event(self) -> str: 94 | stack = [] 95 | opening = 0 96 | closing = 0 97 | 98 | for i, c in enumerate(self.std_output): 99 | if c == "{": 100 | stack.append(i) 101 | if c == "}": 102 | opening = stack.pop() 103 | if len(stack) == 0: 104 | closing = i 105 | break 106 | if closing > opening: 107 | ret = self.std_output[opening:closing+1] 108 | self.std_output = self.std_output[closing+1:] 109 | return ret 110 | else: 111 | return None 112 | 113 | def parse_event(self, e: str) -> str: 114 | event = json.loads(e, strict=False) 115 | 116 | if event["type"] == "logRecord": 117 | return self.parse_l7(event) 118 | if event["type"] == "trace": 119 | return self.parse_trace(event) 120 | if event["type"] == "drop": 121 | return self.parse_drop(event) 122 | if event["type"] == "debug": 123 | return self.parse_debug(event) 124 | if event["type"] == "capture": 125 | return self.parse_capture(event) 126 | if event["type"] == "agent": 127 | return self.parse_agent(event) 128 | 129 | return e 130 | 131 | def parse_labels(self, labels: List[str]) -> str: 132 | return ", ".join([l for l in labels 133 | if "k8s:io.kubernetes.pod.namespace=" not in l]) 134 | 135 | def parse_l7(self, event: Dict) -> str: 136 | src_labels = self.parse_labels(event["srcEpLabels"]) 137 | dst_labels = self.parse_labels(event["dstEpLabels"]) 138 | 139 | action = "" 140 | if "http" in event: 141 | http = event['http'] 142 | action = f"{http['Method']} {http['URL']['Path']}" 143 | 144 | if "kafka" in event: 145 | kafka = event['kafka'] 146 | action = f"{kafka['APIKey']} {kafka['Topic']['Topic']}" 147 | 148 | return (f"({src_labels}) => ({dst_labels}) {event['l7Proto']}" 149 | f" {action} {event['verdict']}") 150 | 151 | def parse_trace(self, event: Dict) -> str: 152 | src_ep, dst_ep = self.get_eps_repr(event) 153 | 154 | return (f"trace ({src_ep}) =>" 155 | f" ({dst_ep})") 156 | 157 | def parse_drop(self, event: Dict) -> str: 158 | src_ep, dst_ep = self.get_eps_repr(event) 159 | 160 | return (f"drop: {event['reason']} ({src_ep}) =>" 161 | f" ({dst_ep})") 162 | 163 | def parse_debug(self, event: Dict) -> str: 164 | return f"debug: {event['message']} on {event['cpu']}" 165 | 166 | def parse_capture(self, event: Dict) -> str: 167 | return f"{event['prefix']}: {event['summary']}" 168 | 169 | def parse_agent(self, event: Dict) -> str: 170 | return f"{event['subtype']}: {event['message']}" 171 | 172 | def get_eps_repr(self, event: Dict) -> Tuple[str, str]: 173 | """ 174 | get_eps_repr returns tuple with source endpoint 175 | and destination endpoint representation 176 | """ 177 | src_repr = "" 178 | dst_repr = "" 179 | src_ip = "" 180 | dst_ip = "" 181 | src_port = "" 182 | dst_port = "" 183 | 184 | try: 185 | src_ip, dst_ip = self.get_ips(event) 186 | except (KeyError, StopIteration): 187 | pass 188 | try: 189 | src_ip, dst_ip = self.get_ips(event) 190 | except KeyError: 191 | pass 192 | 193 | try: 194 | src_port, dst_port = self.get_ports(event) 195 | except (KeyError, StopIteration): 196 | pass 197 | 198 | src_repr = self.get_ep_repr(src_ip, src_port, event.get("source"), 199 | event.get("srcLabel")) 200 | 201 | dst_repr = self.get_ep_repr(dst_ip, dst_port, event.get("dstID"), 202 | event.get("dstLabel")) 203 | return (src_repr, dst_repr) 204 | 205 | def get_ep_repr(self, ip, port, ep_id, identity): 206 | ip_l4 = "" 207 | repr = "" 208 | if ip and port: 209 | ip_l4 = ip + ":" + port 210 | 211 | if ip: 212 | repr = self.resolver.resolve_ip(ip) 213 | 214 | if not repr: 215 | repr = self.resolver.resolve_eid(ep_id) 216 | 217 | if not repr: 218 | labels = self.resolver.resolve_identity(identity) 219 | if labels is not None: 220 | repr = self.parse_labels(labels) 221 | else: 222 | repr = str(identity) 223 | 224 | if ip_l4: 225 | repr += f" {ip_l4}" 226 | elif ip: 227 | repr += f" {ip}" 228 | 229 | return repr 230 | 231 | def get_ips(self, event: Dict) -> Tuple[str, str]: 232 | return (event["summary"]["l3"]["src"], event["summary"]["l3"]["dst"]) 233 | 234 | def get_ports(self, event: Dict) -> Tuple[str, str]: 235 | return (event["summary"]["l4"]["src"], event["summary"]["l4"]["dst"]) 236 | 237 | def __next__(self) -> str: 238 | err = self.get_err() 239 | if err: 240 | return err 241 | 242 | if not self.std_output: 243 | raise StopIteration 244 | 245 | event = self.get_event() 246 | if event is None: 247 | raise StopIteration 248 | 249 | return self.parse_event(event) 250 | -------------------------------------------------------------------------------- /microscope/monitor/runner.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | import json 3 | import sys 4 | from multiprocessing import Queue 5 | 6 | from kubernetes import client 7 | from kubernetes.client.apis import core_v1_api 8 | from kubernetes.client.rest import ApiException 9 | from kubernetes.stream import stream 10 | 11 | from microscope.monitor.monitor import Monitor 12 | from microscope.monitor.epresolver import EndpointResolver 13 | 14 | 15 | class MonitorArgs: 16 | def __init__(self, 17 | verbose: bool, 18 | hex_mode: bool, 19 | related_selectors: List[str], 20 | related_pods: List[str], 21 | related_endpoints: List[int], 22 | to_selectors: List[str], 23 | to_pods: List[str], 24 | to_endpoints: List[int], 25 | from_selectors: List[str], 26 | from_pods: List[str], 27 | from_endpoints: List[int], 28 | types: List[str], 29 | namespace: str, 30 | raw: bool, 31 | related_ips: List[str], 32 | to_ips: List[str], 33 | from_ips: List[str] 34 | ): 35 | self.verbose = verbose 36 | self.hex = hex_mode 37 | self.raw = raw 38 | self.related_selectors = related_selectors 39 | self.related_pods = self.preprocess_pod_names(related_pods) 40 | self.related_endpoints = related_endpoints 41 | self.to_selectors = to_selectors 42 | self.to_pods = self.preprocess_pod_names(to_pods) 43 | self.to_endpoints = to_endpoints 44 | self.from_selectors = from_selectors 45 | self.from_pods = self.preprocess_pod_names(from_pods) 46 | self.from_endpoints = from_endpoints 47 | self.types = types 48 | self.namespace = namespace 49 | self.related_ips = related_ips 50 | self.to_ips = to_ips 51 | self.from_ips = from_ips 52 | 53 | def preprocess_pod_names(self, names: List[str]) -> List[str]: 54 | def defaultize(name: str): 55 | if ':' in name: 56 | return name 57 | else: 58 | return f'{self.namespace}:' + name 59 | return [defaultize(n) for n in names] 60 | 61 | 62 | class MonitorRunner: 63 | def __init__(self, namespace, api, endpoint_namespace): 64 | self.namespace = namespace 65 | self.api = api 66 | self.endpoint_namespace = endpoint_namespace 67 | self.monitors = [] 68 | self.data_queue = Queue() 69 | self.close_queue = Queue() 70 | 71 | def run(self, monitor_args: MonitorArgs, nodes: List[str], 72 | cmd_override: str): 73 | 74 | api = core_v1_api.CoreV1Api() 75 | 76 | try: 77 | pods = api.list_namespaced_pod(self.namespace, 78 | label_selector='k8s-app=cilium') 79 | except ApiException as e: 80 | print('could not list Cilium pods: %s\n' % e) 81 | sys.exit(1) 82 | 83 | if nodes: 84 | names = [(pod.metadata.name, pod.spec.node_name) 85 | for pod in pods.items 86 | if 87 | pod.metadata.name in nodes 88 | or 89 | pod.spec.node_name in nodes] 90 | else: 91 | names = [(pod.metadata.name, pod.spec.node_name) 92 | for pod in pods.items] 93 | 94 | if not names: 95 | raise ValueError('No Cilium nodes in cluster match provided names' 96 | ', or Cilium is not deployed') 97 | 98 | endpoints = self.retrieve_endpoint_data() 99 | pod_resolver = EndpointResolver(endpoints) 100 | 101 | if cmd_override: 102 | cmd = cmd_override.split(" ") 103 | else: 104 | cmd = self.get_monitor_command(monitor_args, names, pod_resolver) 105 | 106 | mode = "" 107 | 108 | if monitor_args.raw: 109 | mode = "raw" 110 | 111 | if monitor_args.verbose or cmd_override: 112 | mode = "verbose" 113 | 114 | self.monitors = [ 115 | Monitor(name[0], name[1], self.namespace, self.data_queue, 116 | self.close_queue, api, cmd, mode, pod_resolver) 117 | for name in names] 118 | 119 | for m in self.monitors: 120 | m.process.start() 121 | 122 | def retrieve_endpoint_info(self, endpoint_data: Dict) -> Dict: 123 | return {x["status"]["id"]: 124 | { 125 | "name": x["metadata"]["name"], 126 | "namespace": x["metadata"]["namespace"], 127 | "networking": x["status"]["status"]["networking"] 128 | } 129 | for x in endpoint_data["items"]} 130 | 131 | def get_monitor_command(self, args: MonitorArgs, names: List[str], 132 | resolver: EndpointResolver) -> List[str]: 133 | related_ids = resolver.resolve_endpoint_ids( 134 | args.related_selectors, 135 | args.related_pods, 136 | args.related_ips, 137 | self.endpoint_namespace) 138 | if (args.related_selectors or args.related_pods) and not related_ids: 139 | raise NoEndpointException("No related endpoints found") 140 | 141 | related_ids.update(args.related_endpoints) 142 | 143 | to_ids = resolver.resolve_endpoint_ids( 144 | args.to_selectors, 145 | args.to_pods, 146 | args.to_ips, 147 | self.endpoint_namespace) 148 | if (args.to_selectors or args.to_pods) and not to_ids: 149 | raise NoEndpointException("No to endpoints found") 150 | 151 | to_ids.update(args.to_endpoints) 152 | 153 | from_ids = resolver.resolve_endpoint_ids( 154 | args.from_selectors, 155 | args.from_pods, 156 | args.from_ips, 157 | self.endpoint_namespace) 158 | if (args.from_selectors or args.from_pods) and not from_ids: 159 | raise NoEndpointException("No from endpoints found") 160 | 161 | from_ids.update(args.from_endpoints) 162 | 163 | exec_command = [ 164 | 'cilium', 165 | 'monitor'] 166 | 167 | if args.verbose: 168 | exec_command.append('-v') 169 | 170 | if args.hex: 171 | if '-v' not in exec_command: 172 | exec_command.append('-v') 173 | exec_command.append('--hex') 174 | 175 | if not args.hex and not args.verbose and not args.raw: 176 | exec_command.append('--json') 177 | 178 | if related_ids: 179 | for e in related_ids: 180 | exec_command.append('--related-to') 181 | exec_command.append(str(e)) 182 | 183 | if to_ids: 184 | for e in to_ids: 185 | exec_command.append('--to') 186 | exec_command.append(str(e)) 187 | 188 | if from_ids: 189 | for e in from_ids: 190 | exec_command.append('--from') 191 | exec_command.append(str(e)) 192 | 193 | if args.types: 194 | for t in args.types: 195 | exec_command.append('--type') 196 | exec_command.append(t) 197 | 198 | print(exec_command) 199 | return exec_command 200 | 201 | def retrieve_endpoint_data(self): 202 | crds = client.CustomObjectsApi() 203 | cep_resp = crds.list_cluster_custom_object("cilium.io", "v2", 204 | "ciliumendpoints") 205 | return [e['status'] for e in cep_resp['items'] if 'status' in e] 206 | 207 | def get_node_endpoint_data(self, node: str): 208 | exec_command = ['cilium', 'endpoint', 'list', '-o', 'json'] 209 | resp = stream(self.api.connect_get_namespaced_pod_exec, node, 210 | self.namespace, 211 | command=exec_command, 212 | stderr=False, stdin=False, 213 | stdout=True, tty=False, _preload_content=False, 214 | _return_http_data_only=True) 215 | output = "" 216 | 217 | # _preload_content causes json to be malformed, 218 | # so we need to load raw data from websocket 219 | while resp.is_open(): 220 | resp.update(timeout=1) 221 | if resp.peek_stdout(): 222 | output += resp.read_stdout() 223 | try: 224 | data = json.loads(output) 225 | resp.close() 226 | except ValueError: 227 | continue 228 | 229 | self.data_queue.put(data) 230 | 231 | def finish(self): 232 | print('\nclosing') 233 | self.close_queue.put('close') 234 | for m in self.monitors: 235 | m.process.join() 236 | 237 | def is_alive(self): 238 | return any([m.process.is_alive() for m in self.monitors]) 239 | 240 | 241 | class NoEndpointException(Exception): 242 | pass 243 | -------------------------------------------------------------------------------- /microscope/monitor/test_monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | from microscope.monitor.parser import MonitorOutputProcessorSimple 3 | from microscope.monitor.parser import MonitorOutputProcessorVerbose 4 | from microscope.monitor.parser import MonitorOutputProcessorJSON 5 | from microscope.monitor.epresolver import EndpointResolver 6 | 7 | 8 | def test_non_verbose_mode(): 9 | p = MonitorOutputProcessorSimple() 10 | 11 | output = """trololo 12 | line2 13 | line3 14 | line4 omg""" 15 | 16 | p.add_out(output) 17 | 18 | msgs = [x for x in p] 19 | assert len(msgs) == 4 20 | 21 | retrieved = "\n".join(msgs) 22 | 23 | assert output == retrieved 24 | 25 | p.add_out(output) 26 | 27 | msgs = [x for x in p] 28 | assert len(msgs) == 4 29 | 30 | retrieved = "\n".join(msgs) 31 | 32 | assert output == retrieved 33 | 34 | p.add_out(output) 35 | p.add_out(output) 36 | 37 | msgs = [x for x in p] 38 | assert len(msgs) == 8 39 | 40 | retrieved = "\n".join(msgs) 41 | 42 | assert output + '\n' + output == retrieved 43 | 44 | 45 | def test_verbose_mode(): 46 | p = MonitorOutputProcessorVerbose() 47 | 48 | output = """trololo 49 | line2 50 | --- 51 | line3 52 | line4 omg 53 | --- 54 | line5 55 | line6 omg""" 56 | 57 | p.add_out(output) 58 | 59 | msgs = [x for x in p] 60 | assert len(msgs) == 2 61 | 62 | assert msgs[0] == output.split("---")[0].strip("\n") 63 | 64 | assert msgs[1] == "---\n" + output.split("---")[1].strip("\n") 65 | 66 | time.sleep(p.last_event_wait_timeout / 1000) 67 | 68 | msgs = [x for x in p] 69 | assert len(msgs) == 1 70 | 71 | assert msgs[0] == "---\n" + output.split("---")[2].strip("\n") 72 | 73 | 74 | def test_json_processor_get_event(): 75 | p = MonitorOutputProcessorJSON(None) 76 | 77 | p.add_out('{"trolo1":"lolo1"}\n') 78 | p.add_out('{"trolo2":"') 79 | 80 | assert p.get_event() == '{"trolo1":"lolo1"}' 81 | assert p.get_event() is None 82 | 83 | p.add_out('lolo2"}\n') 84 | assert p.get_event() == '{"trolo2":"lolo2"}' 85 | 86 | 87 | test_endpoints = [ 88 | { 89 | 'id': 5766, 90 | 'status': { 91 | 'external-identifiers': { 92 | 'pod-name': 'default:app2' 93 | }, 94 | 'networking': {'addressing': [{'ipv4': '10.0.0.1', 95 | 'ipv6': 'f00d::a0f:0:0:1686'}] 96 | }, 97 | 'identity': { 98 | 'id': 21877, 99 | 'labels': ['k8s:id=app2', 100 | 'k8s:io.kubernetes.pod.namespace=default'] 101 | }, 102 | 'labels': { 103 | 'security-relevant': [ 104 | 'k8s:id=app2', 105 | 'k8s:io.kubernetes.pod.namespace=default'] 106 | } 107 | } 108 | }, 109 | { 110 | 'id': 30391, 111 | 'status': { 112 | 'external-identifiers': { 113 | 'pod-name': 'default:app1-799c454b56-xcw8t' 114 | }, 115 | 'networking': {'addressing': [{'ipv4': '10.0.0.2', 116 | 'ipv6': 'f00d::a0f:0:0:1687'}] 117 | }, 118 | 'identity': { 119 | 'id': 50228, 120 | 'labels': ['k8s:id=app1', 121 | 'k8s:io.kubernetes.pod.namespace=default'] 122 | }, 123 | 'labels': { 124 | 'security-relevant': [ 125 | 'k8s:id=app1', 126 | 'k8s:io.kubernetes.pod.namespace=default'] 127 | } 128 | } 129 | }, 130 | { 131 | 'id': 29898, 132 | 'status': { 133 | 'external-identifiers': { 134 | 'pod-name': 'kube-system:cilium-health-minikube' 135 | }, 136 | 'networking': {'addressing': [{'ipv4': '10.0.0.3', 137 | 'ipv6': 'f00d::a0f:0:0:1688'}] 138 | }, 139 | 'identity': { 140 | 'id': 21877, 141 | 'labels': ['reserved:health'] 142 | }, 143 | 'labels': { 144 | 'security-relevant': ['reserved:health'] 145 | } 146 | } 147 | }, 148 | { 149 | 'id': 33243, 150 | 'status': { 151 | 'external-identifiers': { 152 | 'pod-name': 'default:app1-799c454b56-c4q6p' 153 | }, 154 | 'networking': {'addressing': [{'ipv4': '10.0.0.4', 155 | 'ipv6': 'f00d::a0f:0:0:1689'}] 156 | }, 157 | 'identity': { 158 | 'id': 50228, 159 | 'labels': ['k8s:id=app1', 160 | 'k8s:io.kubernetes.pod.namespace=default'] 161 | }, 162 | 'labels': { 163 | 'security-relevant': [ 164 | 'k8s:id=app1', 165 | 'k8s:io.kubernetes.pod.namespace=default'] 166 | } 167 | } 168 | }, 169 | { 170 | 'id': 51796, 171 | 'status': { 172 | 'external-identifiers': { 173 | 'pod-name': 'default:app3' 174 | }, 175 | 'networking': {'addressing': [{'ipv4': '10.0.0.5', 176 | 'ipv6': 'f00d::a0f:0:0:1690'}] 177 | }, 178 | 'identity': { 179 | 'id': 36720, 180 | 'labels': ['k8s:id=app3', 181 | 'k8s:io.kubernetes.pod.namespace=default'], 182 | }, 183 | 'labels': { 184 | 'security-relevant': [ 185 | 'k8s:id=app3', 186 | 'k8s:io.kubernetes.pod.namespace=default'] 187 | } 188 | } 189 | } 190 | ] 191 | 192 | 193 | def test_json_processor(): 194 | resolver = EndpointResolver(test_endpoints) 195 | p = MonitorOutputProcessorJSON(resolver) 196 | 197 | p.add_out('{"type":"logRecord","observationPoint":"Ingress","flowType":') 198 | p.add_out('"Request","l7Proto":"http","srcEpID":0,"srcEpLabels":["k8s:i') 199 | p.add_out('o.kubernetes.pod.namespace=default","k8s:id=app2"],"srcIdent') 200 | p.add_out('ity":3338,"dstEpID":13949,"dstEpLabels":["k8s:id=app1","k8s:') 201 | p.add_out('io.kubernetes.pod.namespace=default"],"DstIdentity":45459,"v') 202 | p.add_out('erdict":"Denied","http":{"Code":403,"Method":"GET","URL":{"S') 203 | p.add_out('cheme":"http","Opaque":"","User":null,"Host":"app1-service",') 204 | p.add_out('"Path":"/private","RawPath":"","ForceQuery":false,"RawQuery"') 205 | p.add_out(':"","Fragment":""},"Protocol":"HTTP/1.1","Headers":{"Accept"') 206 | p.add_out(':["*/*"],"User-Agent":["curl/7.54.0"],"X-Request-Id":["05199') 207 | p.add_out('9d8-6987-4d79-9fad-729c87cb49ae"]}}}') 208 | 209 | p.add_out('{"type":"logRecord","observationPoi') 210 | p.add_out('nt":"Ingress","flowType":"Request",') 211 | p.add_out('"l7Proto":"kafka","srcEpID":0,"srcE') 212 | p.add_out('pLabels":["k8s:app=empire-backup","') 213 | p.add_out('k8s:io.kubernetes.pod.namespace=def') 214 | p.add_out('ault"],"srcIdentity":8370,"dstEpID"') 215 | p.add_out(':29381,"dstEpLabels":["k8s:io.kuber') 216 | p.add_out('netes.pod.namespace=default","k8s:a') 217 | p.add_out('pp=kafka"],"DstIdentity":12427,"ver') 218 | p.add_out('dict":"Forwarded","kafka":{"ErrorCo') 219 | p.add_out('de":0,"APIVersion":5,"APIKey":"fetc') 220 | p.add_out('h","CorrelationID":10,"Topic":{"Top') 221 | p.add_out('ic":"deathstar-plans"}}}') 222 | 223 | p.add_out(""" 224 | { 225 | "cpu": "CPU 01:", 226 | "type": "trace", 227 | "mark": "0xd3f88100", 228 | "ifindex": "lxca3b25", 229 | "state": "reply", 230 | "observationPoint": "to-endpoint", 231 | "traceSummary": "-> endpoint 5766", 232 | "source": 5766, 233 | "bytes": 66, 234 | "srcLabel": 49055, 235 | "dstLabel": 20496, 236 | "dstID": 5766, 237 | "summary": { 238 | "l2":{"src":"22:46:9b:ed:13:e9", "dst":"06:ea:01:96:66:ef"}, 239 | "l3":{"src":"10.0.0.1", "dst":"10.0.0.2"}, 240 | "l4":{"src":"80", "dst":"37934"} 241 | } 242 | } 243 | """) 244 | 245 | p.add_out(""" 246 | { 247 | "cpu": "CPU 00:", 248 | "type": "drop", 249 | "mark": "0xa9263786", 250 | "ifindex": "lxc05f8b", 251 | "reason": "Policy denied (L3)", 252 | "source": 5766, 253 | "bytes": 66, 254 | "srcLabel": 49055, 255 | "dstLabel": 20496, 256 | "dstID": 5766, 257 | "summary": { 258 | "l2":{"src":"22:46:9b:ed:13:e9", "dst":"06:ea:01:96:66:ef"}, 259 | "l3":{"src":"10.0.0.1", "dst":"10.0.0.2"}, 260 | "l4":{"src":"80", "dst":"37934"} 261 | } 262 | } 263 | """) 264 | 265 | p.add_out('{"type":"debug","message":"debug message","cpu":"CPU 01"}') 266 | 267 | p.add_out(""" 268 | { 269 | "cpu": "CPU 01:", 270 | "type": "capture", 271 | "mark": "0x8b0fe309", 272 | "message": "Delivery to ifindex 51", 273 | "source": 29898, 274 | "bytes": 66, 275 | "summary": "capture summary", 276 | "prefix": "-> cilium_health" 277 | } 278 | """) 279 | 280 | p.add_out(""" 281 | { 282 | "type": "agent", 283 | "subtype": "Policy updated", 284 | "message": { 285 | "labels": [ 286 | "unspec:io.cilium.k8s.policy.name=rule1", 287 | "unspec:io.cilium.k8s.policy.namespace=default" 288 | ], 289 | "revision": 10, 290 | "rule_count": 1 291 | } 292 | } 293 | """) 294 | 295 | events = [x for x in p] 296 | 297 | assert len(events) == 7 298 | assert events[0] == ( 299 | "(k8s:id=app2) => (k8s:id=app1) http GET /private Denied" 300 | ) 301 | 302 | assert events[1] == ( 303 | "(k8s:app=empire-backup) => (k8s:app=kafka)" 304 | " kafka fetch deathstar-plans Forwarded" 305 | ) 306 | 307 | assert events[2] == ( 308 | "trace (default:app2 10.0.0.1:80) => (default:app1-799c454b56-xcw8t 10.0.0.2:37934)" # noqa: E501 309 | ) 310 | 311 | assert events[3] == ( 312 | "drop: Policy denied (L3) (default:app2 10.0.0.1:80) => (default:app1-799c454b56-xcw8t 10.0.0.2:37934)" # noqa: E501 313 | ) 314 | 315 | assert events[4] == ( 316 | "debug: debug message on CPU 01" 317 | ) 318 | 319 | assert events[5] == ( 320 | "-> cilium_health: capture summary" 321 | ) 322 | 323 | assert events[6] == ( 324 | "Policy updated: {'labels': ['unspec:io.cilium.k8s.policy.name=rule1', 'unspec:io.cilium.k8s.policy.namespace=default'], 'revision': 10, 'rule_count': 1}" # noqa: E501 325 | 326 | ) 327 | 328 | 329 | def test_resolver_retrieve_ep_ids(): 330 | resolver = EndpointResolver(test_endpoints) 331 | 332 | ids = resolver.resolve_endpoint_ids( 333 | [], [], 334 | ["10.0.0.1", "f00d::a0f:0:0:1687"], "default") 335 | 336 | assert 5766 in ids 337 | assert 30391 in ids 338 | 339 | 340 | def test_resolver_endpoint_ids_by_selectors(): 341 | resolver = EndpointResolver(test_endpoints) 342 | app1_ids = resolver.resolve_endpoint_ids(['id=app1'], [], [], 'default') 343 | 344 | assert 30391 in app1_ids 345 | assert 33243 in app1_ids 346 | assert len(app1_ids) == 2 347 | 348 | 349 | def test_resolver_endpoint_ids_by_names(): 350 | resolver = EndpointResolver(test_endpoints) 351 | ids = resolver.resolve_endpoint_ids( 352 | [], 353 | ['default:app1-799c454b56-xcw8t', 354 | 'default:app3'], 355 | [], 'default') 356 | 357 | assert 30391 in ids 358 | assert 51796 in ids 359 | assert len(ids) == 2 360 | -------------------------------------------------------------------------------- /microscope/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cilium/microscope/db922b79fb28e500f9a2d1f749620485cfda9dc0/microscope/ui/__init__.py -------------------------------------------------------------------------------- /microscope/ui/ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import threading 4 | from typing import Dict 5 | import queue as queuemodule 6 | 7 | import urwid 8 | import urwid.raw_display 9 | 10 | from microscope.monitor.runner import MonitorRunner 11 | from microscope.monitor.monitor import Monitor 12 | 13 | 14 | class MonitorColumn: 15 | def __init__(self, monitor: Monitor): 16 | self.monitor = monitor 17 | self.widget = urwid.Text(monitor.output) 18 | self.last_updated = time.time() 19 | 20 | def set_text(self, text): 21 | self.widget.set_text(text) 22 | self.last_updated = time.time() 23 | 24 | 25 | zoom = False 26 | 27 | 28 | def remove_stale_columns(content: urwid.MonitoredList, 29 | columns: Dict, timeout: int): 30 | if len(columns) == 1 or timeout == 0: 31 | return 32 | now = time.time() 33 | to_remove = [] 34 | for k, c in columns.items(): 35 | if now - c.last_updated > timeout: 36 | content.remove((c.widget, ('weight', 1, False))) 37 | to_remove.append(k) 38 | 39 | for key in to_remove: 40 | del columns[key] 41 | 42 | 43 | def ui(runner: MonitorRunner, empty_column_timeout: int): 44 | monitor_columns = {m.pod_name: MonitorColumn(m) 45 | for m in runner.monitors} 46 | 47 | text_header = (u"Cilium Microscope." 48 | u"UP / DOWN / PAGE UP / PAGE DOWN scroll. F8 exits. " 49 | u"s dumps nodes output to disk. LEFT / RIGHT to switch " 50 | u"columns. z to zoom into column. z again to disable zoom") 51 | 52 | columns = urwid.Columns([c.widget for c in monitor_columns.values()], 53 | 5, min_width=20) 54 | 55 | header = urwid.AttrWrap(urwid.Text(text_header), 'header') 56 | listbox = urwid.ListBox(urwid.SimpleListWalker([columns])) 57 | frame = urwid.Frame(urwid.AttrWrap(listbox, 'body'), header=header) 58 | 59 | palette = [ 60 | ('body', 'black', 'light gray', 'standout'), 61 | ('reverse', 'light gray', 'black'), 62 | ('header', 'white', 'dark red', 'bold'), 63 | ('important', 'dark blue', 'light gray', ('standout', 'underline')), 64 | ('editfc', 'white', 'dark blue', 'bold'), 65 | ('editbx', 'light gray', 'dark blue'), 66 | ('editcp', 'black', 'light gray', 'standout'), 67 | ('bright', 'dark gray', 'light gray', ('bold', 'standout')), 68 | ('buttn', 'black', 'dark cyan'), 69 | ('buttnf', 'white', 'dark blue', 'bold'), 70 | ] 71 | 72 | screen = urwid.raw_display.Screen() 73 | 74 | def dump_data(): 75 | timestamp = time.time() 76 | outputs = {} 77 | for m in runner.monitors: 78 | if m.output_lock.acquire(): 79 | outputs[m.pod_name] = m.output 80 | m.output_lock.release() 81 | 82 | for name, o in outputs.items(): 83 | with open(name + "-" + str(timestamp), 'w') as f: 84 | f.write(o) 85 | 86 | def unhandled(key): 87 | global zoom 88 | if key == 'f8': 89 | raise urwid.ExitMainLoop() 90 | elif key == 's': 91 | dump_data() 92 | elif key == 'right': 93 | columns.focus_position = ((columns.focus_position + 1) 94 | % len(columns.contents)) 95 | elif key == 'left': 96 | columns.focus_position = ((columns.focus_position - 1) 97 | % len(columns.contents)) 98 | elif key == 'z': 99 | width = os.get_terminal_size().columns 100 | if not zoom: 101 | for k, v in enumerate(columns.contents): 102 | columns.contents[k] = (v[0], columns.options("given", 103 | width)) 104 | else: 105 | for k, v in enumerate(columns.contents): 106 | columns.contents[k] = (v[0], columns.options("weight", 1)) 107 | 108 | zoom = not zoom 109 | else: 110 | runner.data_queue.put({}) 111 | 112 | mainloop = urwid.MainLoop(frame, palette, screen, 113 | unhandled_input=unhandled, handle_mouse=False) 114 | 115 | def wait_for_values(monitor_columns, queue, close_queue): 116 | while(close_queue.empty()): 117 | try: 118 | output = queue.get(True, 1) 119 | except queuemodule.Empty: 120 | continue 121 | 122 | if ("name" in output and "output" in output 123 | and output["name"] in monitor_columns): 124 | c = monitor_columns[output["name"]] 125 | if c.monitor.output_lock.acquire(): 126 | c.monitor.output += "\n" + output["output"] 127 | c.set_text(c.monitor.output) 128 | c.monitor.output_lock.release() 129 | 130 | remove_stale_columns(columns.contents, 131 | monitor_columns, empty_column_timeout) 132 | try: 133 | mainloop.draw_screen() 134 | except AssertionError as e: 135 | # this error is encountered when program is closing 136 | # or screen is not ready to draw 137 | # continue so it doesn't clutter the output 138 | continue 139 | 140 | update_thread = threading.Thread(target=wait_for_values, 141 | args=(monitor_columns, 142 | runner.data_queue, 143 | runner.close_queue)) 144 | 145 | # hack to ensure that ssl errors log before mainloop.run call 146 | time.sleep(3) 147 | update_thread.start() 148 | mainloop.run() 149 | update_thread.join() 150 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cachetools==2.0.1 2 | certifi==2017.11.5 3 | chardet==3.0.4 4 | google-auth==1.3.0 5 | idna==2.6 6 | ipaddress==1.0.19 7 | kubernetes==7.0.0 8 | oauthlib==2.0.6 9 | pyasn1==0.4.2 10 | pyasn1-modules==0.2.1 11 | python-dateutil==2.6.1 12 | PyYAML==3.13 13 | requests==2.18.4 14 | requests-oauthlib==0.8.0 15 | rsa==3.4.2 16 | six==1.11.0 17 | urllib3==1.22 18 | urwid==2.0.1 19 | websocket-client==0.53.0 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import sys 5 | import uuid 6 | 7 | from setuptools import setup 8 | try: # for pip >= 10 9 | from pip._internal.req import parse_requirements 10 | except ImportError: # for pip <= 9.0.3 11 | from pip.req import parse_requirements 12 | 13 | version = "1.1.1" 14 | 15 | 16 | with io.open('README.rst', 'r', encoding='utf-8') as readme_file: 17 | readme = readme_file.read() 18 | 19 | if sys.argv[-1] == 'readme': 20 | print(readme) 21 | sys.exit() 22 | 23 | install_reqs = parse_requirements('requirements.txt', session=uuid.uuid1()) 24 | requirements = [str(req.req) for req in install_reqs] 25 | 26 | setup( 27 | name='cilium-microscope', 28 | version=version, 29 | description=('An urwid-based interface for watching ' 30 | '`cilium monitor` events across your cluster'), 31 | long_description=readme, 32 | author='Maciej Kwiek', 33 | author_email='maciej@covalent.io', 34 | url='https://github.com/cilium/microscope', 35 | packages=[ 36 | 'microscope', 'microscope.ui', 'microscope.monitor', 'microscope.batch' 37 | ], 38 | entry_points={ 39 | 'console_scripts': [ 40 | 'microscope = microscope.__main__:main', 41 | ] 42 | }, 43 | python_requires='>=3.5', 44 | include_package_data=True, 45 | install_requires=requirements, 46 | license='Apache 2.0', 47 | zip_safe=True, 48 | classifiers=[ 49 | 'Development Status :: 3 - Alpha', 50 | 'Environment :: Console', 51 | 'Intended Audience :: Developers', 52 | 'Intended Audience :: System Administrators', 53 | 'Natural Language :: English', 54 | 'License :: OSI Approved :: Apache Software License', 55 | 'Programming Language :: Python :: 3', 56 | 'Programming Language :: Python :: 3.5', 57 | 'Programming Language :: Python :: 3.6', 58 | 'Programming Language :: Python :: Implementation :: CPython', 59 | 'Topic :: System :: Systems Administration', 60 | 'Topic :: System :: Networking :: Monitoring', 61 | ], 62 | keywords='microscope, cilium, monitor, k8s, kubernetes', 63 | ) 64 | --------------------------------------------------------------------------------