├── .gitignore ├── LICENSE ├── Makefile ├── README.rst ├── bin ├── env.sh ├── mesos-zsh-completion.sh └── test.sh ├── docs ├── architecture.rst └── howto-write-command.rst ├── mesos ├── __init__.py └── cli │ ├── __init__.py │ ├── cfg.py │ ├── cli.py │ ├── cluster.py │ ├── cmds │ ├── __init__.py │ ├── cat.py │ ├── completion.py │ ├── config.py │ ├── events.py │ ├── find.py │ ├── frameworks.py │ ├── head.py │ ├── help.py │ ├── ls.py │ ├── ps.py │ ├── resolve.py │ ├── scp.py │ ├── ssh.py │ ├── state.py │ └── tail.py │ ├── completion_helpers.py │ ├── exceptions.py │ ├── framework.py │ ├── log.py │ ├── main.py │ ├── master.py │ ├── mesos_file.py │ ├── parallel.py │ ├── parser.py │ ├── slave.py │ ├── task.py │ ├── util.py │ └── zookeeper.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── data │ ├── browse.json │ ├── config.json │ ├── master-host │ ├── master.pb │ ├── master_state.json │ ├── sandbox │ │ ├── foo │ │ │ └── bar │ │ ├── log │ │ ├── stderr │ │ └── stdout │ ├── slave-20140619-151434-16842879-5050-1196-0.json │ └── slave_statistics.json ├── integration │ ├── __init__.py │ ├── test_cat.py │ ├── test_completion.py │ ├── test_config.py │ ├── test_events.py │ ├── test_find.py │ ├── test_head.py │ ├── test_ls.py │ ├── test_ps.py │ ├── test_resolve.py │ ├── test_scp.py │ ├── test_ssh.py │ ├── test_state.py │ └── test_tail.py ├── unit │ ├── __init__.py │ ├── test_master.py │ └── test_slave.py └── utils.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | *.rpm 22 | *.deb 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | .idea/ 36 | .mesos.json 37 | .pypirc 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013 Mesosphere 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | env: 3 | bin/env.sh 4 | test: 5 | bin/test.sh 6 | .PHONY: fix-isort 7 | fix-isort: 8 | isort -rc . 9 | 10 | .PHONY: clean 11 | clean: 12 | rm -rf *.egg 13 | rm -rf .tox 14 | rm -rf build 15 | find . -name "*.pyc" -delete 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | mesos-cli 3 | ========= 4 | 5 | **This project has been deprecated and is no longer being actively maintained. Please use the** `DC/OS CLI`_. 6 | 7 | CLI tools to work with mesos. 8 | 9 | ----------------------------- 10 | What problem does this solve? 11 | ----------------------------- 12 | 13 | I am comfortable debugging programs from the command line. I have a set of tools that I use from coreutils that end up being used every day. A day doesn't go by when I've not used grep, find, or cat. 14 | 15 | While mesos allows you to treat all the nodes in your data center as anonymous resources, debugging still needs to be done on specific hosts. Currently, it requires multiple tools and context switches to gather the pieces that you need to look at what a specific task is doing. Most of these existing tools don't work well with the command line and I'm unable to use the workflow that I've become comfortable with. 16 | 17 | -------------------------- 18 | How is the problem solved? 19 | -------------------------- 20 | 21 | To solve the problem, some of the coreutil commands have been re-implemented to work across the entire data center instead of a single host. 22 | 23 | - commands and options have been copied as closely as it makes sense. They should have the same options that you're used to. 24 | - pipe works the way you'd expect it to. You should be able to replace most host specific debug scripts easily. 25 | - mesos itself isn't required locally. Developers want to debug their tasks without having a local copy of mesos installed. 26 | - everything is task centric. There's no need to worry about specifying a framework if you don't want to, just put in the task id. 27 | - lazy matching. Task IDs are long and normally require cut/paste to get exact matches. Instead, all `task` parameters are partial matches, no need to type that long thing in. 28 | - auto-complete. Most parameters tab-complete (see the section on auto-completion to configure), just type a couple characters in and get what you're looking for. 29 | - extensibility. Write your own subcommands. Most of the required information can be accessed via. existing subcommands (leading master resolution via. mesos-resolve), so you only need to implement what you want and not re-invent the wheel. 30 | 31 | ------- 32 | Install 33 | ------- 34 | 35 | Note that if you've already installed `mesos` locally, you can either install this to a location other than `/usr/local/bin` via. pip options or remove `/usr/local/bin/mesos`. There should be no downsides to just removing it. 36 | 37 | From PyPI: 38 | 39 | .. code-block:: bash 40 | 41 | pip install mesos.cli 42 | 43 | From this repo: 44 | 45 | .. code-block:: bash 46 | 47 | python setup.py install 48 | 49 | ------------------ 50 | Create Environment 51 | ------------------ 52 | 53 | .. code-block:: bash 54 | 55 | make env 56 | source env/bin/activate 57 | 58 | ----- 59 | Build 60 | ----- 61 | 62 | .. code-block:: bash 63 | 64 | python setup.py build 65 | 66 | ---- 67 | Test 68 | ---- 69 | 70 | .. code-block:: bash 71 | 72 | make test 73 | 74 | ------------------- 75 | Command Completion 76 | ------------------- 77 | 78 | Task IDs? File names? Complete all the things! Configure command completion and you'll be able to tab complete most everything. 79 | 80 | +++++ 81 | BASH 82 | +++++ 83 | 84 | Add the following to your startup scripts: 85 | 86 | .. code-block:: bash 87 | 88 | complete -C mesos-completion mesos 89 | 90 | ++++ 91 | ZSH 92 | ++++ 93 | 94 | Add the following to your `.zshrc`: 95 | 96 | .. code-block:: bash 97 | 98 | source mesos-zsh-completion.sh 99 | 100 | Note that `bashcompinit` is being used. If you're running an older version of ZSH, it won't work. Take a look at `bin/mesos-zsh-completion.sh` for information. 101 | 102 | ------------- 103 | Configuration 104 | ------------- 105 | 106 | Place a configuration file at any of the following: 107 | 108 | .. code-block:: bash 109 | 110 | ./.mesos.json 111 | ~/.mesos.json 112 | /etc/.mesos.json 113 | /usr/etc/.mesos.json 114 | /usr/local/etc/.mesos.json 115 | 116 | You can override the location of this config via. `MESOS_CLI_CONFIG`. 117 | 118 | If you're using a non-local master, you'll need to configure where the master should be found like so: 119 | 120 | .. code-block:: bash 121 | 122 | mesos config master zk://localhost:2181/mesos 123 | 124 | Alternatively, you can create the config file yourself. 125 | 126 | .. code-block:: json 127 | 128 | { 129 | "profile": "default", 130 | "default": { 131 | "master": "zk://localhost:2181/mesos", 132 | "log_level": "warning", 133 | "log_file": "/tmp/mesos-cli.log" 134 | } 135 | } 136 | 137 | Note that master accepts all values that mesos normally does, eg: 138 | 139 | .. code-block:: bash 140 | 141 | localhost:5050 142 | zk://localhost:2181/mesos 143 | file:///path/to/config/above 144 | 145 | +++++++++ 146 | Profiles 147 | +++++++++ 148 | 149 | Want to access multiple mesos clusters without changing config? You're in luck! 150 | 151 | To change your profile, you can run: 152 | 153 | .. code-block:: bash 154 | 155 | mesos config profile new-profile 156 | 157 | The old config will be maintained and can be switched back to at any point. 158 | 159 | +++++++++++++++ 160 | Config Options 161 | +++++++++++++++ 162 | 163 | .. code-block:: json 164 | 165 | { 166 | // Show stack traces on keyboard interrupt 167 | "debug": "false", 168 | 169 | // Path to where you'd like the log file 170 | "log_file": None, 171 | 172 | // Log level to use. 173 | "log_level": "warning", 174 | 175 | // Location of your master, this can be any of the values that mesos 176 | // supports which includes the following: 177 | // localhost:5050 178 | // zk://localhost:2181/mesos 179 | // file:///path/to/config 180 | "master": "localhost:5050", 181 | 182 | // Scheme to use when connecting to mesos, can be either http or https 183 | "scheme": "http" 184 | } 185 | 186 | ======== 187 | Commands 188 | ======== 189 | 190 | All commands have their own options and parameters. Make sure you run `mesos [command] --help` to get the potential options. 191 | 192 | Most commands take a `task-id` as parameter. This does not need to be an exact match and for commands where it makes sense, can match multiple tasks. Suppose your cluster is running the following tasks: 193 | 194 | hadoop.myjob.12345-1928731 195 | 196 | rails.48271236-1231234 197 | 198 | app-10.89934ht-2398hriwuher 199 | 200 | app-20.9845uih-9823hriu-2938u422 201 | 202 | - A task-id of `app` will match both app-10 and app-20. 203 | - A task-id of `myjob` will only match the hadoop task. 204 | - A task-id of `1231234` will only match the rails task. 205 | 206 | --- 207 | cat 208 | --- 209 | 210 | .. code-block:: bash 211 | 212 | mesos cat task-id file [file] 213 | 214 | ------ 215 | events 216 | ------ 217 | 218 | .. code-block:: bash 219 | 220 | mesos events 221 | 222 | observe events from the cluster. You will see the events occurring on the master and all slaves in the cluster (including new slaves as they arrive) as they occur. 223 | 224 | ---- 225 | find 226 | ---- 227 | 228 | .. code-block:: bash 229 | 230 | mesos find task-id [path] 231 | 232 | When multiple tasks match task-id, headers will be printed between their results. 233 | 234 | ---- 235 | head 236 | ---- 237 | 238 | .. code-block:: bash 239 | 240 | mesos head -n 10 task-id file [file] 241 | 242 | -- 243 | ls 244 | -- 245 | 246 | .. code-block:: bash 247 | 248 | mesos ls task-id [path] 249 | 250 | The default view is `ls -la`. When multiple tasks match task-id, headers will be printed between their results. 251 | 252 | -- 253 | ps 254 | -- 255 | 256 | .. code-block:: bash 257 | 258 | mesos ps 259 | 260 | Output time, memory, cpu, command, user and slave/task_id information for currently running tasks. 261 | 262 | --- 263 | scp 264 | --- 265 | 266 | .. code-block:: bash 267 | 268 | mesos scp file [file ...] remote_path 269 | 270 | Upload local file(s) to the remote_path on every slave. Note that you will need to have SSH access to every slave you'd like to upload to. 271 | 272 | --- 273 | ssh 274 | --- 275 | 276 | .. code-block:: bash 277 | 278 | mesos ssh task-id 279 | 280 | This will SSH into the sandbox of the specified task on the slave that it is running on. Note that you need to have SSH access to this slave/sandbox. 281 | 282 | ---- 283 | tail 284 | ---- 285 | 286 | .. code-block:: tail 287 | 288 | mesos tail -n 10 task-id file [file] 289 | 290 | This also implements follow. Unlike normal tail, it will look for tasks/files being created on your mesos cluster and begin to follow those files as they are written to. You can start tail in --follow mode and then launch your tasks to watch everything has it happens. 291 | 292 | =============== 293 | Adding Commands 294 | =============== 295 | 296 | Commands are all separate scripts. The `mesos` script inspects your path and looks for everything that starts with `mesos-`. To add a new command, just name the script `mesos-new-name` and you'll have a new command. This makes it possible to write new sub-commands in whatever language you'd like. 297 | 298 | There are some utils that are nice to have when you're doing a new command. While all of them are available in python via. this package, a subset is available via. existing commands. This allows you to focus on the new functionality you'd like in your command (in the language you're comfortable with). 299 | 300 | ------ 301 | config 302 | ------ 303 | 304 | .. code-block:: bash 305 | 306 | mesos config [key] [value] 307 | 308 | 309 | Output a json object containing all the mesos-cli config or you can get/set specific values in the configuration. 310 | 311 | ------- 312 | resolve 313 | ------- 314 | 315 | .. code-block:: bash 316 | 317 | mesos resolve [master-config] 318 | 319 | Take either the existing configured master or the one passed on the command line and discover where the leading master is. You'll be able to use the following format: 320 | 321 | .. code-block:: bash 322 | 323 | localhost:5050 324 | zk://localhost:2181/mesos 325 | file:///path/to/config/above 326 | 327 | ----- 328 | state 329 | ----- 330 | 331 | .. code-block:: bash 332 | 333 | mesos state [slave-id] 334 | 335 | Return the full JSON state of either the master or slave (partial matches are valid). 336 | 337 | ======= 338 | Testing 339 | ======= 340 | 341 | There are two ways to do testing. If you'd like to just test with your local setup: 342 | 343 | python setup.py nosetests 344 | 345 | For a full virtualenv + specific python versions (py26, py27), you can use tox: 346 | 347 | tox 348 | 349 | .. _`DC/OS CLI`: https://github.com/dcos/dcos-cli 350 | -------------------------------------------------------------------------------- /bin/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | BASEDIR=`dirname $0`/.. 4 | 5 | if [ ! -d "$BASEDIR/env" ]; then 6 | 7 | virtualenv -q $BASEDIR/env --prompt='(mesos-cli) ' 8 | echo "Virtualenv created." 9 | 10 | source $BASEDIR/env/bin/activate 11 | echo "Virtualenv activated." 12 | 13 | pip install -r $BASEDIR/requirements.txt 14 | pip install -e $BASEDIR 15 | echo "Requirements installed." 16 | 17 | elif [ ! -f "$BASEDIR/env/bin/activate" -o "$BASEDIR/setup.py" -nt "$BASEDIR/env/bin/activate" ]; then 18 | 19 | source $BASEDIR/env/bin/activate 20 | echo "Virtualenv activated." 21 | 22 | pip install -r $BASEDIR/requirements.txt 23 | pip install -e $BASEDIR 24 | echo "Requirements installed." 25 | 26 | fi 27 | -------------------------------------------------------------------------------- /bin/mesos-zsh-completion.sh: -------------------------------------------------------------------------------- 1 | # Source this file to enable auto-completion in ZSH. 2 | # 3 | # Note that a ZSH version containing this commit is required: 4 | # https://github.com/zsh-users/zsh/commit/edab1d3dbe61da7efe5f1ac0e40444b2ec9b9570 5 | 6 | autoload -Uz bashcompinit compinit 7 | compinit 8 | bashcompinit -i 9 | 10 | _bash_complete() { 11 | local ret=1 12 | local -a suf matches 13 | local -x COMP_POINT COMP_CWORD 14 | local -a COMP_WORDS COMPREPLY BASH_VERSINFO 15 | local -x COMP_LINE="$words" 16 | local -A savejobstates savejobtexts 17 | 18 | (( COMP_POINT = 1 + ${#${(j. .)words[1,CURRENT]}} + $#QIPREFIX + $#IPREFIX + $#PREFIX )) 19 | (( COMP_CWORD = CURRENT - 1)) 20 | COMP_WORDS=( $words ) 21 | BASH_VERSINFO=( 2 05b 0 1 release ) 22 | 23 | savejobstates=( ${(kv)jobstates} ) 24 | savejobtexts=( ${(kv)jobtexts} ) 25 | 26 | [[ ${argv[${argv[(I)nospace]:-0}-1]} = -o ]] && suf=( -S '' ) 27 | 28 | matches=( ${(f)"$(compgen $@ -- ${words[CURRENT]})"} ) 29 | 30 | if [[ -n $matches ]]; then 31 | if [[ ${argv[${argv[(I)filenames]:-0}-1]} = -o ]]; then 32 | compset -P '*/' && matches=( ${matches##*/} ) 33 | compset -S '/*' && matches=( ${matches%%/*} ) 34 | compadd -Q -f "${suf[@]}" -a matches && ret=0 35 | else 36 | compadd -Q "${suf[@]}" -a matches && ret=0 37 | fi 38 | fi 39 | 40 | if (( ret )); then 41 | if [[ ${argv[${argv[(I)default]:-0}-1]} = -o ]]; then 42 | _default "${suf[@]}" && ret=0 43 | elif [[ ${argv[${argv[(I)dirnames]:-0}-1]} = -o ]]; then 44 | _directories "${suf[@]}" && ret=0 45 | fi 46 | fi 47 | 48 | return ret 49 | } 50 | 51 | complete -o nospace -C mesos-completion mesos 52 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | BASEDIR=`dirname $0`/.. 4 | 5 | cd $BASEDIR 6 | $BASEDIR/env/bin/tox 7 | -------------------------------------------------------------------------------- /docs/architecture.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Architecture 3 | ============== 4 | 5 | --------------------- 6 | High Level Decisions 7 | --------------------- 8 | 9 | - commands and options have been copied as closely as it makes sense. They should have the same options that you're used to. 10 | - pipe works the way you'd expect it to. You should be able to replace most host specific debug scripts easily. 11 | - mesos itself isn't required locally. Developers want to debug their tasks without having a local copy of mesos installed. 12 | - everything is task centric. There's no need to worry about specifying a framework if you don't want to, just put in the task id. 13 | - lazy matching. Task IDs are long and normally require cut/paste to get exact matches. Instead, all `task` parameters are partial matches, no need to type that long thing in. 14 | - auto-complete. Most parameters tab-complete (see the section on auto-completion to configure), just type a couple characters in and get what you're looking for. 15 | - extensibility. Write your own subcommands. Most of the required information can be accessed via. existing subcommands (leading master resolution via. mesos-resolve), so you only need to implement what you want and not re-invent the wheel. 16 | 17 | - Concurrency is done via. `concurrent.futures`. 18 | - All interactions that can be done in parallel, should be done that way (fetching slave state, files, ...) 19 | 20 | ------------------- 21 | Language Decisions 22 | ------------------- 23 | 24 | - PEP8 is followed strictly. flake8 is used for verification as part of the test process. 25 | - python 2.6 and 2.7 are officially supported. 26 | - wherever possible, python 3 support is used (print statements, concurrent) 27 | 28 | --------- 29 | Commands 30 | --------- 31 | 32 | - All commands are `mesos-command-name`. 33 | - There is a `mesos` command that handles command discovery. It doesn't care what language commands have been written in. The PATH is looked at for any command containing `mesos-`. 34 | - Each command is its own script. These manifest as entry points in `setup.py`. 35 | - The `main` function in every command is what gets executed. 36 | - Command specific arguments use `argparse`. 37 | 38 | -------------- 39 | Configuration 40 | -------------- 41 | 42 | - Global options (master config, logging) go into a config file. 43 | - Config files have a search path (see cfg.py). 44 | - Default values are set, config file overrides them. 45 | 46 | +++++++++++ 47 | Profiles 48 | +++++++++++ 49 | 50 | - You can create a profile within the config and switch between it. 51 | 52 | --------------- 53 | Master + Slave 54 | --------------- 55 | 56 | - The master class has a CURRENT singleton that should always be used. 57 | 58 | ++++++++++++++++++ 59 | Cached Properties 60 | ++++++++++++++++++ 61 | 62 | - Any property that requires a remote request (master.state for example) uses util.CachedProperty. This will memoize it until the ttl has elapsed. 63 | - Properties that do not change (slave.log) should be memoized and not cached. 64 | 65 | ------------ 66 | Mesos Files 67 | ------------ 68 | 69 | - Files provided by mesos have their own file implementation. 70 | - The implementation provides a feature rich python class that lets you do all the normal file operations on it. 71 | 72 | --------------- 73 | Tab Completion 74 | --------------- 75 | 76 | - The library being used for tab completion hoooks into `argparse` 77 | - For arguments that require looking up something remotely, a function is provided. 78 | -------------------------------------------------------------------------------- /docs/howto-write-command.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Write a new command 3 | ========================== 4 | 5 | 1. Go to `entry_points` in `setup.py` and add your command:: 6 | 7 | 'mesos-my-command = mesos.cli.cmds.my_command:main' 8 | 9 | 2. Create the command file:: 10 | 11 | touch mesos/cli/cmds/my_command.py 12 | 13 | 3. Add the basic structure:: 14 | 15 | from __future__ import absolute_import, print_function 16 | 17 | from .. import cli 18 | 19 | parser = cli.parser( 20 | description="what my command does" 21 | ) 22 | 23 | @cli.init(parser) 24 | def main(args): 25 | pass 26 | 27 | 4. Implement your task! 28 | 29 | - There are helpers for most common command line arguments, see `parser.py` for a list. 30 | - Looking for master state? Take a look at `mesos.cli.cmds.state` for the pattern to use. 31 | - Doing something with files? You can probably use `mesos.cli.cluster`. Take a peek at `mesos.cli.cmds.cat` for an example of how to use it. 32 | - Interested in performance data? Look at `mesos.cli.cmds.ps`. 33 | 34 | 5. Create a test:: 35 | 36 | touch tests/integration/test_my_command.py 37 | 38 | 6. Setup the skeleton:: 39 | 40 | from __future__ import absolute_import, print_function 41 | 42 | import mock 43 | 44 | import mesos.cli.cmds.my_command 45 | 46 | from .. import utils 47 | 48 | 49 | class TestCat(utils.MockState): 50 | pass 51 | 52 | 7. Write the test! 53 | 54 | - Master state and slave state are mocked out. You shouldn't need to do anything special. 55 | - When calling each test, you will need to patch the args. Take a look at `tests/integration/test_cat.py` for an example. 56 | 57 | 8. Verify style:: 58 | 59 | make test 60 | 61 | 9. Test against all python versions:: 62 | 63 | tox 64 | 65 | -------------------------------------------------------------------------------- /mesos/__init__.py: -------------------------------------------------------------------------------- 1 | # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages 2 | try: 3 | __import__('pkg_resources').declare_namespace(__name__) 4 | except ImportError: 5 | from pkgutil import extend_path 6 | __path__ = extend_path(__path__, __name__) 7 | -------------------------------------------------------------------------------- /mesos/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | __version__ = '0.1.5' 19 | -------------------------------------------------------------------------------- /mesos/cli/cfg.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import errno 20 | import json 21 | import os 22 | 23 | 24 | class Config(object): 25 | 26 | _default_profile = "default" 27 | 28 | DEFAULTS = { 29 | "debug": "false", 30 | "log_file": None, 31 | "log_level": "warning", 32 | "master": "localhost:5050", 33 | "max_workers": 5, 34 | "scheme": "http", 35 | "response_timeout": 5 36 | } 37 | 38 | cfg_name = ".mesos.json" 39 | 40 | _default_config_location = os.path.join(os.path.expanduser("~"), cfg_name) 41 | 42 | search_path = [os.path.join(x, cfg_name) for x in [ 43 | ".", 44 | os.path.expanduser("~"), 45 | "/etc", 46 | "/usr/etc", 47 | "/usr/local/etc" 48 | ]] 49 | 50 | def __init__(self): 51 | self.__items = {self._default_profile: self.DEFAULTS} 52 | self["profile"] = self._default_profile 53 | 54 | self.load() 55 | 56 | def __str__(self): 57 | return json.dumps(self.__items, indent=4) 58 | 59 | def _config_file(self): 60 | for path in self.search_path: 61 | if os.path.exists(path): 62 | return path 63 | 64 | # default to creating a user level config file 65 | return self._default_config_location 66 | 67 | def _get_path(self): 68 | return os.environ.get( 69 | 'MESOS_CLI_CONFIG', self._config_file()) 70 | 71 | @property 72 | def _profile_key(self): 73 | return self.__items.get("profile", self._default_profile) 74 | 75 | @property 76 | def _profile(self): 77 | return self.__items.get(self._profile_key, {}) 78 | 79 | def __getitem__(self, item): 80 | if item == "profile": 81 | return self.__items[item] 82 | return self._profile.get(item, self.DEFAULTS[item]) 83 | 84 | def __setitem__(self, k, v): 85 | if k == "profile": 86 | self.__items[k] = v 87 | return 88 | 89 | profile = self._profile 90 | profile[k] = v 91 | self.__items[self._profile_key] = profile 92 | 93 | def load(self): 94 | try: 95 | with open(self._get_path(), 'rt') as f: 96 | try: 97 | data = json.load(f) 98 | except ValueError as e: 99 | raise ValueError( 100 | 'Invalid %s JSON: %s [%s]' % 101 | (type(self).__name__, e.message, self._get_path()) 102 | ) 103 | self.__items.update(data) 104 | except IOError as e: 105 | if e.errno != errno.ENOENT: 106 | raise 107 | 108 | def save(self): 109 | with open(self._get_path(), "wb") as f: 110 | f.write(str(self)) 111 | 112 | CURRENT = Config() 113 | -------------------------------------------------------------------------------- /mesos/cli/cli.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import argparse 21 | import functools 22 | import logging 23 | import os 24 | 25 | import blessings 26 | 27 | import mesos.cli 28 | 29 | from . import log 30 | from .cfg import CURRENT as CFG 31 | from .parser import ArgumentParser 32 | 33 | 34 | def init(arg): 35 | # arg is: 36 | # @cli.init : the function to be decorated 37 | # @cli.init(parser) : the parsing function 38 | is_parser = not callable(arg) 39 | 40 | def decorator(fn): 41 | @handle_signals 42 | @log.duration 43 | @functools.wraps(fn) 44 | def wrapper(*args, **kwargs): 45 | log_level = getattr(logging, CFG["log_level"].upper()) 46 | logging.basicConfig( 47 | level=log_level, 48 | filename=CFG["log_file"] 49 | ) 50 | 51 | if CFG["debug"] == "true": 52 | debug_requests() 53 | 54 | cmd_args = arg.parse_args() if is_parser else None 55 | return fn(cmd_args, *args, **kwargs) 56 | return wrapper 57 | 58 | if callable(arg): 59 | return decorator(arg) 60 | else: 61 | return decorator 62 | 63 | 64 | def parser(**kwargs): 65 | parser_inst = ArgumentParser( 66 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 67 | **kwargs 68 | ) 69 | 70 | parser_inst.add_argument( 71 | "-v", "--version", 72 | action="version", version="%(prog)s {0}".format(mesos.cli.__version__) 73 | ) 74 | return parser_inst 75 | 76 | 77 | def handle_signals(fn): 78 | @functools.wraps(fn) 79 | def wrapper(*args, **kwargs): 80 | try: 81 | return fn(*args, **kwargs) 82 | except KeyboardInterrupt: 83 | if CFG["debug"] == "true": 84 | raise 85 | return wrapper 86 | 87 | 88 | def header(name): 89 | term = blessings.Terminal() 90 | print("==>" + term.red(str(name)) + "<==") 91 | 92 | 93 | def cmds(short=False): 94 | def fltr(cmd): 95 | if not cmd.startswith("mesos-"): 96 | return False 97 | if cmd.endswith(".sh"): 98 | return False 99 | return True 100 | 101 | cmds = set([]) 102 | for path in os.environ.get("PATH").split(os.pathsep): 103 | try: 104 | cmds = cmds.union(filter(fltr, os.listdir(path))) 105 | except OSError: 106 | pass 107 | 108 | if short: 109 | cmds = [x.split("-", 1)[-1] for x in cmds] 110 | 111 | return sorted(cmds) 112 | 113 | last_seen = None 114 | 115 | 116 | def output_file(seq, show_header=True, key=None): 117 | global last_seen 118 | 119 | if not key: 120 | key = str(seq) 121 | 122 | first = True 123 | for line in seq: 124 | if first and key != last_seen and show_header: 125 | header(key) 126 | 127 | # TODO(thomasr) - It is possible for there to be a pause in 128 | # the middle of this loop (reading the next block from the 129 | # remote) in this case, the header wouln't be printed and the 130 | # user would be confused. 131 | print(line) 132 | 133 | first = False 134 | 135 | if not first: 136 | last_seen = key 137 | 138 | 139 | def debug_requests(): 140 | try: 141 | import http.client as http_client 142 | except ImportError: 143 | # Python 2 144 | import httplib as http_client 145 | http_client.HTTPConnection.debuglevel = 1 146 | -------------------------------------------------------------------------------- /mesos/cli/cluster.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import itertools 21 | 22 | from . import cli, exceptions, log, parallel 23 | from .master import CURRENT as MASTER 24 | 25 | dne = True 26 | missing_slave = set([]) 27 | 28 | 29 | def files(fn, fltr, flist, active_only=False, fail=True): 30 | global dne 31 | 32 | tlist = MASTER.tasks(fltr, active_only=active_only) 33 | dne = True 34 | 35 | def process((task, fname)): 36 | global dne 37 | 38 | try: 39 | fobj = task.file(fname) 40 | except exceptions.SlaveDoesNotExist: 41 | if task["id"] not in missing_slave: 42 | cli.header("{0}:{1}".format( 43 | task["id"], fname)) 44 | print("Slave no longer exists.") 45 | 46 | missing_slave.add(task["id"]) 47 | raise exceptions.SkipResult 48 | 49 | if fobj.exists(): 50 | 51 | dne = False 52 | return fn(fobj) 53 | 54 | elements = itertools.chain( 55 | *[[(task, fname) for fname in flist] for task in tlist]) 56 | 57 | for result in parallel.stream(process, elements): 58 | if not result: 59 | continue 60 | yield result 61 | 62 | if dne and fail: 63 | log.fatal("No such task has the requested file or directory") 64 | -------------------------------------------------------------------------------- /mesos/cli/cmds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesosphere-backup/mesos-cli/c65c8bcc3087a063d437698014e30dfd165a5257/mesos/cli/cmds/__init__.py -------------------------------------------------------------------------------- /mesos/cli/cmds/cat.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | from .. import cli, cluster 21 | 22 | parser = cli.parser( 23 | description="concatenate and print files" 24 | ) 25 | 26 | parser.task_argument() 27 | parser.file_argument() 28 | 29 | parser.add_argument( 30 | "-i", "--inactive", action="store_true", 31 | help="show inactive tasks as well" 32 | ) 33 | 34 | @cli.init(parser) 35 | def main(args): 36 | for (fname, lines) in cluster.files( 37 | lambda fobj: (str(fobj), list(fobj)), 38 | args.task, 39 | args.file, 40 | active_only=not args.inactive): 41 | cli.output_file(lines, False, key=fname) 42 | -------------------------------------------------------------------------------- /mesos/cli/cmds/completion.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import importlib 21 | import os 22 | import sys 23 | 24 | from .. import cli 25 | 26 | """Provide tab completions for python subcommands. 27 | 28 | To debug, add `_ARC_DEBUG` to your env. 29 | """ 30 | 31 | EXIT = sys.exit 32 | 33 | 34 | def complete_cmd(name=""): 35 | print("\n".join([x for x in cli.cmds(short=True) if x.startswith(name)])) 36 | 37 | 38 | def cmd_options(cmd): 39 | os.environ["_ARGCOMPLETE_IFS"] = "\n" 40 | os.environ["_ARGCOMPLETE_WORDBREAKS"] = os.environ.get( 41 | "COMP_WORDBREAKS", "") 42 | os.environ["_ARGCOMPLETE"] = "2" 43 | 44 | try: 45 | mod = importlib.import_module( 46 | ".{0}".format(cmd), package="mesos.cli.cmds") 47 | except ImportError: 48 | return 49 | 50 | if not hasattr(mod, 'parser'): 51 | return 52 | 53 | importlib.import_module("argcomplete").autocomplete( 54 | mod.parser, 55 | output_stream=sys.stdout, 56 | exit_method=EXIT 57 | ) 58 | 59 | 60 | def usage(): 61 | print("""Please look at the README for instructions on setting command 62 | completion up for your shell.""") 63 | 64 | 65 | @cli.init 66 | def main(args): 67 | cmdline = os.environ.get('COMP_LINE') or \ 68 | os.environ.get('COMMAND_LINE') or '' 69 | cmdpoint = int(os.environ.get('COMP_POINT') or len(cmdline)) 70 | 71 | words = cmdline[:cmdpoint].split() 72 | 73 | if len(words) == 0: 74 | return usage() 75 | elif len(words) == 1: 76 | return complete_cmd() 77 | elif len(words) == 2: 78 | if cmdline[-1] == " ": 79 | return cmd_options(words[1]) 80 | else: 81 | return complete_cmd(words[1]) 82 | else: 83 | return cmd_options(words[1]) 84 | -------------------------------------------------------------------------------- /mesos/cli/cmds/config.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | from .. import cli 20 | from ..cfg import CURRENT as CFG 21 | 22 | parser = cli.parser( 23 | description="interact with your local cli configuration" 24 | ) 25 | 26 | parser.add_argument( 27 | "key", nargs="?", choices=CFG.DEFAULTS.keys() + ["profile"]) 28 | 29 | parser.add_argument("value", nargs="?") 30 | 31 | 32 | @cli.init(parser) 33 | def main(args): 34 | if args.key: 35 | if args.value: 36 | CFG[args.key] = args.value 37 | CFG.save() 38 | else: 39 | print(CFG[args.key]) 40 | else: 41 | print(CFG) 42 | -------------------------------------------------------------------------------- /mesos/cli/cmds/events.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import os 20 | import time 21 | 22 | import concurrent.futures 23 | 24 | from .. import cli, parallel 25 | from ..master import CURRENT as MASTER 26 | 27 | parser = cli.parser( 28 | description="observe events from the cluster" 29 | ) 30 | 31 | parser.add_argument( 32 | '-s', '--sleep-interval', type=float, default=5, 33 | help="Sleep approximately N seconds between iterations" 34 | ) 35 | 36 | parser.enable_print_header() 37 | 38 | # Testing helpers 39 | FOLLOW = True 40 | POSITION = os.SEEK_END 41 | 42 | 43 | # TODO(thomasr) - Should operate identical to tail, output the last couple 44 | # lines before beginning to follow. 45 | @cli.init(parser) 46 | def main(args): 47 | with parallel.execute() as executor: 48 | active_streams = set() 49 | jobs = set() 50 | 51 | def read_log(log, sleep=args.sleep_interval): 52 | time.sleep(sleep) 53 | cli.output_file(log, not args.q) 54 | return log 55 | 56 | def add_reader(log): 57 | log.seek(0, POSITION) 58 | active_streams.add(log) 59 | jobs.add(executor.submit(read_log, log, 0)) 60 | 61 | def find_slaves(): 62 | for slave in MASTER.slaves(): 63 | if slave.log not in active_streams: 64 | add_reader(slave.log) 65 | 66 | add_reader(MASTER.log) 67 | while True: 68 | done, jobs = concurrent.futures.wait( 69 | jobs, return_when=concurrent.futures.FIRST_COMPLETED) 70 | for job in done: 71 | jobs.add(executor.submit(read_log, job.result())) 72 | find_slaves() 73 | 74 | if not FOLLOW: 75 | break 76 | -------------------------------------------------------------------------------- /mesos/cli/cmds/find.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | 22 | from .. import cli, completion_helpers, exceptions 23 | from ..master import CURRENT as MASTER 24 | 25 | parser = cli.parser( 26 | description="List all the files inside a specific task's sandbox" 27 | ) 28 | 29 | parser.add_argument( 30 | 'task', type=str, 31 | help="""Name of the task. 32 | 33 | Note that this can be a partial match.""" 34 | ).completer = completion_helpers.task 35 | 36 | parser.add_argument( 37 | 'path', type=str, nargs="?", default="", 38 | help="""Path to view.""" 39 | ).completer = completion_helpers.file 40 | 41 | parser.enable_print_header() 42 | 43 | 44 | @cli.init(parser) 45 | def main(args): 46 | tlist = MASTER.tasks(args.task) 47 | path = args.path 48 | if path.endswith("/"): 49 | path = path[:-1] 50 | 51 | for task in tlist: 52 | try: 53 | base = os.path.join(task.directory, path) 54 | flist = task.file_list(path) 55 | except exceptions.SlaveDoesNotExist: 56 | continue 57 | 58 | def walk_dir(flist): 59 | for file_meta in flist: 60 | print(os.path.relpath(file_meta["path"], base)) 61 | if file_meta["mode"][0].startswith("d"): 62 | walk_dir(task.file_list(file_meta["path"])) 63 | 64 | if len(tlist) > 1 and not args.q: 65 | cli.header(task) 66 | if len(flist) > 0: 67 | walk_dir(flist) 68 | -------------------------------------------------------------------------------- /mesos/cli/cmds/frameworks.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import blessings 21 | import prettytable 22 | 23 | from .. import cli 24 | from ..master import CURRENT as MASTER 25 | 26 | try: 27 | from collections import OrderedDict 28 | except ImportError: 29 | from ordereddict import OrderedDict 30 | 31 | parser = cli.parser( 32 | description="List frameworks with statistics about their active tasks \ 33 | and resource allocation." 34 | ) 35 | 36 | parser.add_argument( 37 | "-i", "--inactive", action="store_true", 38 | help="show inactive frameworks as well" 39 | ) 40 | 41 | @cli.init(parser) 42 | def main(args): 43 | term = blessings.Terminal() 44 | 45 | table_generator = OrderedDict([ 46 | ("ID", lambda x: x.id), 47 | ("name", lambda x: x.name), 48 | ("host", lambda x: x.hostname), 49 | ("active", lambda x: x.active), 50 | ("tasks", lambda x: x.task_count), 51 | ("cpu", lambda x: x.cpu_allocated), 52 | ("mem", lambda x: x.mem_allocated), 53 | ("disk", lambda x: x.disk_allocated), 54 | ]) 55 | 56 | tb = prettytable.PrettyTable( 57 | [x.upper() for x in table_generator.keys()], 58 | border=False, 59 | max_table_width=term.width, 60 | hrules=prettytable.NONE, 61 | vrules=prettytable.NONE, 62 | left_padding_width=0, 63 | right_padding_width=1 64 | ) 65 | 66 | def format_row(framework): 67 | return [fn(framework) for fn in table_generator.values()] 68 | 69 | for framework in MASTER.frameworks(active_only=not args.inactive): 70 | tb.add_row(format_row(framework)) 71 | 72 | if tb.rowcount == 0: 73 | cli.header('You have no frameworks') 74 | else: 75 | print(tb) 76 | 77 | -------------------------------------------------------------------------------- /mesos/cli/cmds/head.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import itertools 21 | 22 | from .. import cli, cluster 23 | 24 | parser = cli.parser( 25 | description="display first lines of a file" 26 | ) 27 | 28 | parser.task_argument() 29 | parser.file_argument() 30 | 31 | parser.add_argument( 32 | "-i", "--inactive", action="store_true", 33 | help="show inactive tasks as well" 34 | ) 35 | 36 | parser.add_argument( 37 | '-n', default=10, type=int, 38 | help="Number of lines of the file to output." 39 | ) 40 | 41 | parser.enable_print_header() 42 | 43 | 44 | @cli.init(parser) 45 | def main(args): 46 | 47 | def read_file(fobj): 48 | return (str(fobj), list(itertools.islice(fobj, args.n))) 49 | 50 | for (fname, lines) in cluster.files( 51 | read_file, 52 | args.task, 53 | args.file, 54 | active_only=not args.inactive): 55 | cli.output_file(lines, not args.q, key=fname) 56 | -------------------------------------------------------------------------------- /mesos/cli/cmds/help.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | from .. import cli 21 | 22 | 23 | USAGE = """Usage: mesos [OPTIONS] 24 | 25 | Available commands: 26 | \t{cmds} 27 | """ 28 | 29 | 30 | @cli.init 31 | def main(args=None): 32 | print(USAGE.format(cmds="\n\t".join(cli.cmds(short=True)))) 33 | -------------------------------------------------------------------------------- /mesos/cli/cmds/ls.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import datetime 21 | import os 22 | 23 | from .. import cli, exceptions 24 | from ..master import CURRENT as MASTER 25 | 26 | parser = cli.parser( 27 | description="List all the files inside a specific task's sandbox" 28 | ) 29 | 30 | parser.task_argument() 31 | parser.path_argument() 32 | parser.enable_print_header() 33 | 34 | 35 | def format_line(obj, base): 36 | human_time = datetime.datetime.fromtimestamp(obj["mtime"]).strftime( 37 | "%b %d %H:%M") 38 | fmt = "{mode} {nlink: >3} {uid} {gid} {size: >5} {human_time} {fname}" 39 | fname = os.path.relpath(obj["path"], base) 40 | return fmt.format(human_time=human_time, fname=fname, **obj) 41 | 42 | 43 | @cli.init(parser) 44 | def main(args): 45 | tlist = MASTER.tasks(args.task) 46 | for task in tlist: 47 | if len(tlist) > 1 and not args.q: 48 | cli.header(task) 49 | 50 | p = args.path 51 | if p.endswith("/"): 52 | p = p[:-1] 53 | 54 | try: 55 | for fobj in task.file_list(p): 56 | print(format_line( 57 | fobj, os.path.join(task.directory, args.path))) 58 | except exceptions.SlaveDoesNotExist: 59 | continue 60 | -------------------------------------------------------------------------------- /mesos/cli/cmds/ps.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import blessings 21 | import prettytable 22 | 23 | from .. import cli, exceptions, parallel, util 24 | from ..master import CURRENT as MASTER 25 | 26 | try: 27 | from collections import OrderedDict 28 | except ImportError: 29 | from ordereddict import OrderedDict 30 | 31 | 32 | parser = cli.parser( 33 | description="process status" 34 | ) 35 | 36 | parser.add_argument( 37 | "-i", "--inactive", action="store_true", 38 | help="show inactive tasks as well" 39 | ) 40 | 41 | parser.task_argument(optional=True) 42 | 43 | 44 | def get_memory(x): 45 | max_mem = x["resources"]["mem"] * 1024 * 1024 * 1.0 46 | if max_mem > 0: 47 | return "{0:.2f}".format((x.rss / max_mem) * 100) 48 | return "0" 49 | 50 | 51 | @cli.init(parser) 52 | def main(args): 53 | term = blessings.Terminal() 54 | 55 | table_generator = OrderedDict([ 56 | # user_time + system_time 57 | ("time", lambda x: x.cpu_time), 58 | ("state", lambda x: x["state"].split("_")[-1][0]), 59 | # mem_rss 60 | ("rss", lambda x: util.humanize_bytes(x.rss)), 61 | # cpus_limit 62 | ("cpu", lambda x: x.cpu_limit), 63 | # mem_rss / mem_limit 64 | ("%mem", get_memory), 65 | # executor.name 66 | ("command", lambda x: x.command), 67 | ("user", lambda x: x.user), 68 | # task_id 69 | ("id", lambda x: x["id"]), 70 | ]) 71 | 72 | tb = prettytable.PrettyTable( 73 | [x.upper() for x in table_generator.keys()], 74 | border=False, 75 | max_table_width=term.width, 76 | hrules=prettytable.NONE, 77 | vrules=prettytable.NONE, 78 | left_padding_width=0, 79 | right_padding_width=1 80 | ) 81 | 82 | def format_row(task): 83 | try: 84 | return [fn(task) for fn in table_generator.values()] 85 | except exceptions.SlaveDoesNotExist: 86 | return None 87 | 88 | for row in parallel.by_slave( 89 | format_row, 90 | MASTER.tasks(active_only=(not args.inactive), fltr=args.task)): 91 | if row: 92 | tb.add_row(row) 93 | 94 | if tb.rowcount == 0: 95 | print("===>{0}You have no tasks for that filter{1}<===".format( 96 | term.red, term.white)) 97 | else: 98 | print(tb) 99 | -------------------------------------------------------------------------------- /mesos/cli/cmds/resolve.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | from .. import cli 20 | from ..cfg import CURRENT as CFG 21 | from ..master import CURRENT as MASTER 22 | 23 | parser = cli.parser( 24 | description="return the host/port for the currently leading master." 25 | ) 26 | 27 | parser.add_argument( 28 | "master", nargs="?", default=CFG["master"] 29 | ) 30 | 31 | 32 | @cli.init(parser) 33 | def main(args): 34 | print(MASTER.resolve(args.master)) 35 | -------------------------------------------------------------------------------- /mesos/cli/cmds/scp.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import itertools 21 | import os 22 | import subprocess 23 | 24 | from .. import cli, log, parallel 25 | from ..master import CURRENT as MASTER 26 | 27 | parser = cli.parser( 28 | description="upload the specified local file(s) to all slaves" 29 | ) 30 | 31 | parser.add_argument( 32 | "file", nargs="+", 33 | help="Local files to upload to the slaves" 34 | ) 35 | 36 | parser.add_argument( 37 | "remote_path", 38 | help="Remote path to upload local files to" 39 | ) 40 | 41 | 42 | @cli.init(parser) 43 | def main(args): 44 | def upload((slave, src)): 45 | cmd = [ 46 | "scp", 47 | "-pr", 48 | src, 49 | "{0}:{1}".format(slave["hostname"], args.remote_path) 50 | ] 51 | try: 52 | return (slave, src, log.fn(subprocess.check_call, cmd)) 53 | except subprocess.CalledProcessError, e: 54 | return (slave, e.returncode) 55 | 56 | with parallel.execute() as executor: 57 | file_tuples = lambda slave: [(slave, fname) for fname in args.file] 58 | 59 | upload_jobs = executor.map(upload, itertools.chain( 60 | *[file_tuples(slave) for slave in MASTER.slaves()])) 61 | 62 | for slave, src, retcode in upload_jobs: 63 | print("{0}:{1}\t{2}".format( 64 | slave["hostname"], os.path.join(args.remote_path, src), 65 | "uploaded" if retcode == 0 else "failed")) 66 | -------------------------------------------------------------------------------- /mesos/cli/cmds/ssh.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import os 20 | import platform 21 | 22 | import blessings 23 | 24 | from .. import cli, completion_helpers, log 25 | from ..master import CURRENT as MASTER 26 | 27 | parser = cli.parser( 28 | description="SSH into the sandbox of a specific task" 29 | ) 30 | 31 | parser.add_argument( 32 | 'task', type=str, 33 | help="""Name of the task.""" 34 | ).completer = completion_helpers.task 35 | 36 | 37 | @cli.init(parser) 38 | def main(args): 39 | term = blessings.Terminal() 40 | 41 | # There's a security bug in Mavericks wrt. urllib2: 42 | # http://bugs.python.org/issue20585 43 | if platform.system() == "Darwin": 44 | os.environ["no_proxy"] = "*" 45 | 46 | task = MASTER.task(args.task) 47 | 48 | cmd = [ 49 | "ssh", 50 | "-t", 51 | task.slave["hostname"], 52 | "cd {0} && bash".format(task.directory) 53 | ] 54 | if task.directory == "": 55 | print(term.red + "warning: the task no longer exists on the " + 56 | "target slave. Will not enter sandbox" + term.white + "\n\n") 57 | cmd = cmd[:-1] 58 | 59 | log.fn(os.execvp, "ssh", cmd) 60 | -------------------------------------------------------------------------------- /mesos/cli/cmds/state.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import json 21 | 22 | from .. import cli, completion_helpers 23 | from ..master import CURRENT as MASTER 24 | 25 | parser = cli.parser( 26 | description="fetch the json state for either the master or a specific " + 27 | "slave" 28 | ) 29 | 30 | parser.add_argument( 31 | "slave", nargs="?", 32 | help="ID of the slave. May match multiple slaves (or all)" 33 | ).completer = completion_helpers.slave 34 | 35 | 36 | @cli.init(parser) 37 | def main(args): 38 | if not args.slave: 39 | print(json.dumps(MASTER.state, indent=4)) 40 | else: 41 | print(json.dumps( 42 | [s.state for s in MASTER.slaves(args.slave)], indent=4)) 43 | -------------------------------------------------------------------------------- /mesos/cli/cmds/tail.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import functools 21 | import itertools 22 | import time 23 | 24 | from .. import cli, cluster 25 | 26 | RECHECK = 1 27 | 28 | parser = cli.parser( 29 | description="display the last part of a file" 30 | ) 31 | 32 | parser.task_argument() 33 | parser.file_argument() 34 | 35 | parser.add_argument( 36 | "-i", "--inactive", action="store_true", 37 | help="show inactive tasks as well" 38 | ) 39 | 40 | parser.add_argument( 41 | '-f', '--follow', action='store_true', 42 | help="Wait for additional data to be appended to the file." 43 | ) 44 | 45 | parser.add_argument( 46 | '-n', default=10, type=int, 47 | help="Number of lines of the file to tail." 48 | ) 49 | 50 | parser.enable_print_header() 51 | 52 | files_seen = {} 53 | 54 | 55 | def until_end(fobj): 56 | global files_seen 57 | 58 | fobj.seek(files_seen.get(fobj, 0)) 59 | return list(fobj) 60 | 61 | 62 | def read_file(fn, fobj): 63 | global files_seen 64 | 65 | lines = fn(fobj) 66 | files_seen[fobj] = fobj.tell() 67 | 68 | return (str(fobj), lines) 69 | 70 | 71 | @cli.init(parser) 72 | def main(args): 73 | 74 | def last_lines(fobj): 75 | return reversed(list(itertools.islice(reversed(fobj), args.n))) 76 | 77 | def output(fn): 78 | for (fname, lines) in cluster.files( 79 | functools.partial(read_file, fn), 80 | args.task, 81 | args.file, 82 | active_only=not args.inactive, 83 | fail=(not args.follow)): 84 | cli.output_file( 85 | lines, not args.q, key=fname) 86 | 87 | output(last_lines) 88 | 89 | if args.follow: 90 | while True: 91 | output(until_end) 92 | time.sleep(RECHECK) 93 | -------------------------------------------------------------------------------- /mesos/cli/completion_helpers.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import os 19 | 20 | from . import exceptions 21 | from .master import CURRENT as MASTER 22 | 23 | 24 | def task(prefix, parsed_args, **kwargs): 25 | return [x["id"] for x in MASTER.tasks(prefix)] 26 | 27 | 28 | def slave(prefix, parsed_args, **kwargs): 29 | return [s["id"] for s in MASTER.slaves(prefix)] 30 | 31 | 32 | def file(prefix, parsed_args, **kwargs): 33 | files = set([]) 34 | split = prefix.rsplit("/", 1) 35 | base = "" 36 | if len(split) == 2: 37 | base = split[0] 38 | pattern = split[-1] 39 | 40 | for task in MASTER.tasks(parsed_args.task): 41 | # It is possible for the master to have completed tasks that no longer 42 | # have files and/or executors 43 | try: 44 | for file_meta in task.file_list(base): 45 | rel = os.path.relpath(file_meta["path"], task.directory) 46 | if rel.rsplit("/", 1)[-1].startswith(pattern): 47 | if file_meta["mode"].startswith("d"): 48 | rel += "/" 49 | files.add(rel) 50 | except exceptions.MissingExecutor: 51 | pass 52 | return files 53 | -------------------------------------------------------------------------------- /mesos/cli/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | 4 | # Licensed to the Apache Software Foundation (ASF) under one 5 | # or more contributor license agreements. See the NOTICE file 6 | # distributed with this work for additional information 7 | # regarding copyright ownership. The ASF licenses this file 8 | # to you under the Apache License, Version 2.0 (the 9 | # "License"); you may not use this file except in compliance 10 | # with the License. You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | 21 | class MesosCLIException(Exception): 22 | pass 23 | 24 | 25 | class FileDoesNotExist(MesosCLIException): 26 | pass 27 | 28 | 29 | class MissingExecutor(MesosCLIException): 30 | pass 31 | 32 | 33 | class SlaveDoesNotExist(MesosCLIException): 34 | pass 35 | 36 | 37 | class SkipResult(MesosCLIException): 38 | pass 39 | -------------------------------------------------------------------------------- /mesos/cli/framework.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | class Framework(object): 21 | def __init__(self, items): 22 | self.__items = items 23 | 24 | def __getitem__(self, name): 25 | return self.__items[name] 26 | 27 | def __str__(self): 28 | return "{0}:{1}".format(self.name, self.id) 29 | 30 | @property 31 | def id(self): 32 | return self['id'] 33 | 34 | @property 35 | def name(self): 36 | return self['name'] 37 | 38 | @property 39 | def hostname(self): 40 | return self['hostname'] 41 | 42 | @property 43 | def active(self): 44 | return self['active'] 45 | 46 | @property 47 | def task_count(self): 48 | return len(self['tasks']) 49 | 50 | @property 51 | def user(self): 52 | return self['user'] 53 | 54 | @property 55 | def cpu_allocated(self): 56 | return self._resource_allocated("cpus") 57 | 58 | @property 59 | def mem_allocated(self): 60 | return self._resource_allocated("mem") 61 | 62 | @property 63 | def disk_allocated(self): 64 | return self._resource_allocated("disk") 65 | 66 | def _resource_allocated(self, resource): 67 | return self["resources"][resource] 68 | 69 | -------------------------------------------------------------------------------- /mesos/cli/log.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import functools 21 | import logging 22 | import sys 23 | import time 24 | 25 | debug = logging.debug 26 | 27 | 28 | def fatal(msg, code=1): 29 | sys.stdout.write(msg + "\n") 30 | logging.error(msg) 31 | sys.exit(code) 32 | 33 | 34 | def fn(f, *args, **kwargs): 35 | logging.debug("{0}: {1} {2}".format(repr(f), args, kwargs)) 36 | return f(*args, **kwargs) 37 | 38 | 39 | def duration(fn): 40 | @functools.wraps(fn) 41 | def timer(*args, **kwargs): 42 | start = time.time() 43 | try: 44 | return fn(*args, **kwargs) 45 | finally: 46 | debug("duration: {0}.{1}: {2:2.2f}s".format( 47 | fn.__module__, 48 | fn.__name__, 49 | time.time() - start)) 50 | 51 | return timer 52 | -------------------------------------------------------------------------------- /mesos/cli/main.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | import sys 22 | 23 | from . import cli, log 24 | 25 | 26 | FAILURE_MESSAGE = """'{}' is not a valid command (or cannot be found) 27 | 28 | To see a list of commands, run `mesos help`.""" 29 | 30 | 31 | def main(): 32 | if len(sys.argv) == 1: 33 | cmd = "mesos-help" 34 | else: 35 | cmd = "mesos-" + sys.argv[1] 36 | 37 | if cmd in cli.cmds(): 38 | log.fn(os.execvp, cmd, [cmd] + sys.argv[2:]) 39 | else: 40 | log.fatal(FAILURE_MESSAGE.format(cmd)) 41 | -------------------------------------------------------------------------------- /mesos/cli/master.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import fnmatch 21 | import itertools 22 | import json 23 | import os 24 | import re 25 | import urlparse 26 | 27 | import google.protobuf.message 28 | import kazoo.client 29 | import kazoo.exceptions 30 | import kazoo.handlers.threading 31 | import requests 32 | import requests.exceptions 33 | 34 | import mesos.interface.mesos_pb2 35 | 36 | from . import exceptions, framework, log, mesos_file, slave, task, util, zookeeper 37 | from .cfg import CURRENT as CFG 38 | 39 | ZOOKEEPER_TIMEOUT = 1 40 | 41 | INVALID_PATH = "{0} does not have a valid path. Did you forget /mesos?" 42 | 43 | MISSING_MASTER = """unable to connect to a master at {0}. 44 | 45 | Try running `mesos config master zk://localhost:2181/mesos`. See the README for 46 | more examples.""" 47 | 48 | MULTIPLE_SLAVES = "There are multiple slaves with that id. Please choose one: " 49 | 50 | 51 | class MesosMaster(object): 52 | 53 | def __str__(self): 54 | return "".format(self.key()) 55 | 56 | def key(self): 57 | return CFG["master"] 58 | 59 | @util.CachedProperty() 60 | def host(self): 61 | return "{0}://{1}".format(CFG["scheme"], self.resolve(CFG["master"])) 62 | 63 | @log.duration 64 | def fetch(self, url, **kwargs): 65 | try: 66 | return requests.get( 67 | urlparse.urljoin(self.host, url), 68 | timeout=CFG["response_timeout"], 69 | **kwargs) 70 | except requests.exceptions.ConnectionError: 71 | log.fatal(MISSING_MASTER.format(self.host)) 72 | 73 | def _file_resolver(self, cfg): 74 | return self.resolve(open(cfg[6:], "r+").read().strip()) 75 | 76 | def _zookeeper_resolver(self, cfg): 77 | hosts, path = cfg[5:].split("/", 1) 78 | path = "/" + path 79 | 80 | with zookeeper.client(hosts=hosts, read_only=True) as zk: 81 | try: 82 | def master_id(key): 83 | return int(key.split("_")[-1]) 84 | 85 | def get_masters(): 86 | return [x for x in zk.get_children(path) 87 | if re.search("\d+", x)] 88 | 89 | leader = sorted(get_masters(), key=lambda x: master_id(x)) 90 | 91 | if len(leader) == 0: 92 | log.fatal("cannot find any masters at {0}".format(cfg,)) 93 | data, stat = zk.get(os.path.join(path, leader[0])) 94 | except kazoo.exceptions.NoNodeError: 95 | log.fatal(INVALID_PATH.format(cfg)) 96 | 97 | if not data: 98 | log.fatal("Cannot retrieve valid MasterInfo data from ZooKeeper") 99 | 100 | # The serialization of the Master's PID to ZK has changed over time. 101 | # Prior to 0.20 just the PID value was written to the znode; then the binary 102 | # serialization of the `MasterInfo` Protobuf was written and then, as of 0.24.x, 103 | # the protobuf is actually serialized as JSON. 104 | # We try all strategies, starting with the most recent, falling back if we cannot 105 | # parse the value. 106 | 107 | # Try JSON first: 108 | try: 109 | parsed = json.loads(data) 110 | if parsed and "address" in parsed: 111 | ip = parsed["address"].get("ip") 112 | port = parsed["address"].get("port") 113 | if ip and port: 114 | return "{ip}:{port}".format(ip=ip, port=port) 115 | except ValueError as parse_error: 116 | log.debug("[WARN] No JSON content, probably connecting to older Mesos version. " 117 | "Reason: {}".format(parse_error)) 118 | 119 | # Try to deserialize the MasterInfo protobuf next: 120 | try: 121 | info = mesos.interface.mesos_pb2.MasterInfo() 122 | info.ParseFromString(data) 123 | return info.pid 124 | except google.protobuf.message.DecodeError as parse_error: 125 | log.debug("[WARN] Cannot deserialize Protobuf either, probably connecting to a " 126 | "legacy Mesos version, please consider upgrading. Reason: {}".format( 127 | parse_error)) 128 | # Finally try to just interpret the PID as a string: 129 | if '@' in data: 130 | return data.split("@")[-1] 131 | log.fatal("Could not retrieve the MasterInfo from ZooKeeper; the data in '{}' was:" 132 | "{}".format(path, data)) 133 | 134 | @log.duration 135 | def resolve(self, cfg): 136 | """Resolve the URL to the mesos master. 137 | 138 | The value of cfg should be one of: 139 | - host:port 140 | - zk://host1:port1,host2:port2/path 141 | - zk://username:password@host1:port1/path 142 | - file:///path/to/file (where file contains one of the above) 143 | """ 144 | if cfg.startswith("zk:"): 145 | return self._zookeeper_resolver(cfg) 146 | elif cfg.startswith("file:"): 147 | return self._file_resolver(cfg) 148 | else: 149 | return cfg 150 | 151 | @util.CachedProperty(ttl=5) 152 | def state(self): 153 | return self.fetch("/master/state.json").json() 154 | 155 | @util.memoize 156 | def slave(self, fltr): 157 | lst = self.slaves(fltr) 158 | 159 | log.debug("master.slave({0})".format(fltr)) 160 | 161 | if len(lst) == 0: 162 | raise exceptions.SlaveDoesNotExist( 163 | "Slave {0} no longer exists.".format(fltr)) 164 | 165 | elif len(lst) > 1: 166 | result = [MULTIPLE_SLAVES] 167 | result += ['\t{0}'.format(slave.id) for slave in lst] 168 | log.fatal('\n'.join(result)) 169 | 170 | return lst[0] 171 | 172 | def slaves(self, fltr=""): 173 | return list(map( 174 | lambda x: slave.MesosSlave(x), 175 | itertools.ifilter( 176 | lambda x: fltr in x["id"], self.state["slaves"]))) 177 | 178 | def _task_list(self, active_only=False): 179 | keys = ["tasks"] 180 | if not active_only: 181 | keys.append("completed_tasks") 182 | return itertools.chain( 183 | *[util.merge(x, *keys) for x in self._framework_list(active_only)]) 184 | 185 | def task(self, fltr): 186 | lst = self.tasks(fltr) 187 | 188 | if len(lst) == 0: 189 | log.fatal("Cannot find a task by that name.") 190 | 191 | elif len(lst) > 1: 192 | msg = ["There are multiple tasks with that id. Please choose one:"] 193 | msg += ["\t{0}".format(t["id"]) for t in lst] 194 | log.fatal("\n".join(msg)) 195 | 196 | return lst[0] 197 | 198 | # XXX - need to filter on task state as well as id 199 | def tasks(self, fltr="", active_only=False): 200 | return list(map( 201 | lambda x: task.Task(self, x), 202 | itertools.ifilter( 203 | lambda x: fltr in x["id"] or fnmatch.fnmatch(x["id"], fltr), 204 | self._task_list(active_only)))) 205 | 206 | def framework(self, fwid): 207 | return list(filter( 208 | lambda x: x.id == fwid, 209 | self.frameworks()))[0] 210 | 211 | def _framework_list(self, active_only=False): 212 | keys = ["frameworks"] 213 | if not active_only: 214 | keys.append("completed_frameworks") 215 | return util.merge(self.state, *keys) 216 | 217 | def frameworks(self, active_only=False): 218 | keys = ["frameworks"] 219 | if not active_only: 220 | keys.append("completed_frameworks") 221 | return list(map(lambda x: framework.Framework(x), self._framework_list(active_only))) 222 | 223 | @property 224 | @util.memoize 225 | def log(self): 226 | return mesos_file.File(self, path="/master/log") 227 | 228 | CURRENT = MesosMaster() 229 | -------------------------------------------------------------------------------- /mesos/cli/mesos_file.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | 22 | from . import exceptions, util 23 | 24 | 25 | class File(object): 26 | 27 | chunk_size = 1024 28 | 29 | def __init__(self, host, task=None, path=None): 30 | self.host = host 31 | self.task = task 32 | self.path = path 33 | 34 | if self.task is None: 35 | self._host_path = self.path 36 | else: 37 | self._host_path = os.path.join(self.task.directory, self.path) 38 | 39 | self._offset = 0 40 | 41 | # Used during fetch, class level so the dict isn't constantly alloc'd 42 | self._params = { 43 | "path": self._host_path, 44 | "offset": -1, 45 | "length": self.chunk_size 46 | } 47 | 48 | def __iter__(self): 49 | for line in self._readlines(): 50 | yield line 51 | 52 | def __eq__(self, y): 53 | return self.key() == y.key() 54 | 55 | def __hash__(self): 56 | return hash(self.__str__()) 57 | 58 | def __repr__(self): 59 | return "".format(self.path, self._where) 60 | 61 | def __str__(self): 62 | return "{0}:{1}".format(self._where, self.path) 63 | 64 | def key(self): 65 | return "{0}:{1}".format(self.host.key(), self._host_path) 66 | 67 | @property 68 | def _where(self): 69 | return self.task["id"] if self.task is not None else self.host.key() 70 | 71 | def __reversed__(self): 72 | for i, line in enumerate(self._readlines_reverse()): 73 | # Don't include the terminator when reading in reverse. 74 | if i == 0 and line == "": 75 | continue 76 | yield line 77 | 78 | def _fetch(self): 79 | resp = self.host.fetch("/files/read.json", params=self._params) 80 | if resp.status_code == 404: 81 | raise exceptions.FileDoesNotExist("No such file or directory.") 82 | return resp.json() 83 | 84 | def exists(self): 85 | try: 86 | self.size 87 | return True 88 | except exceptions.FileDoesNotExist: 89 | return False 90 | except exceptions.SlaveDoesNotExist: 91 | return False 92 | 93 | # When reading a file, it is common to first check whether it exists, then 94 | # look at the size to determine where to seek. Instead of requiring 95 | # multiple requests to the slave, the size is cached for a very short 96 | # period of time. 97 | @util.CachedProperty(ttl=0.5) 98 | def size(self): 99 | return self._fetch()["offset"] 100 | 101 | def seek(self, offset, whence=os.SEEK_SET): 102 | if whence == os.SEEK_SET: 103 | self._offset = 0 + offset 104 | elif whence == os.SEEK_CUR: 105 | self._offset += offset 106 | elif whence == os.SEEK_END: 107 | self._offset = self.size + offset 108 | 109 | def tell(self): 110 | return self._offset 111 | 112 | def _length(self, start, size): 113 | if size and self.tell() - start + self.chunk_size > size: 114 | return size - (self.tell() - start) 115 | return self.chunk_size 116 | 117 | def _get_chunk(self, loc, size=None): 118 | if size is None: 119 | size = self.chunk_size 120 | 121 | self.seek(loc, os.SEEK_SET) 122 | self._params["offset"] = loc 123 | self._params["length"] = size 124 | 125 | data = self._fetch()["data"] 126 | self.seek(len(data), os.SEEK_CUR) 127 | return data 128 | 129 | def _read(self, size=None): 130 | start = self.tell() 131 | 132 | fn = lambda: self._get_chunk( 133 | self.tell(), 134 | size=self._length(start, size)) 135 | pre = lambda x: x == "" 136 | post = lambda x: size and (self.tell() - start) >= size 137 | 138 | for blob in util.iter_until(fn, pre, post): 139 | yield blob 140 | 141 | def _read_reverse(self, size=None): 142 | fsize = self.size 143 | if not size: 144 | size = fsize 145 | 146 | def next_block(): 147 | current = fsize 148 | while (current - self.chunk_size) > (fsize - size): 149 | current -= self.chunk_size 150 | yield current 151 | 152 | for pos in next_block(): 153 | yield self._get_chunk(pos) 154 | 155 | yield self._get_chunk(fsize - size, size % self.chunk_size) 156 | 157 | def read(self, size=None): 158 | return ''.join(self._read(size)) 159 | 160 | def readline(self, size=None): 161 | for line in self._readlines(size): 162 | return line 163 | 164 | def _readlines(self, size=None): 165 | last = "" 166 | for blob in self._read(size): 167 | 168 | # This is not streaming and assumes small chunk sizes 169 | blob_lines = (last + blob).split("\n") 170 | for line in blob_lines[:len(blob_lines) - 1]: 171 | yield line 172 | 173 | last = blob_lines[-1] 174 | 175 | def _readlines_reverse(self, size=None): 176 | buf = "" 177 | for blob in self._read_reverse(size): 178 | 179 | blob_lines = (blob + buf).split("\n") 180 | for line in reversed(blob_lines[1:]): 181 | yield line 182 | 183 | buf = blob_lines[0] 184 | yield buf 185 | 186 | def readlines(self, size=None): 187 | return list(self._readlines(size)) 188 | -------------------------------------------------------------------------------- /mesos/cli/parallel.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import contextlib 21 | import itertools 22 | 23 | import concurrent.futures 24 | 25 | from . import exceptions 26 | from .cfg import CURRENT as CFG 27 | 28 | 29 | @contextlib.contextmanager 30 | def execute(): 31 | try: 32 | executor = concurrent.futures.ThreadPoolExecutor( 33 | max_workers=CFG["max_workers"]) 34 | yield executor 35 | except KeyboardInterrupt: 36 | # Threads in the ThreadPoolExecutor are created with 37 | # daemon=True. There is, therefore, an atexit function registered 38 | # that allows all the currently running threads to stop before 39 | # allowing the interpreter to stop. Because we don't care whether 40 | # the worker threads exit cleanly or not, we force shutdown to be 41 | # immediate. 42 | concurrent.futures.thread._threads_queues.clear() 43 | raise 44 | finally: 45 | executor.shutdown(wait=False) 46 | 47 | 48 | def stream(fn, elements): 49 | """Yield the results of fn as jobs complete.""" 50 | jobs = [] 51 | 52 | with execute() as executor: 53 | for elem in elements: 54 | jobs.append(executor.submit(fn, elem)) 55 | 56 | for job in concurrent.futures.as_completed(jobs): 57 | try: 58 | yield job.result() 59 | except exceptions.SkipResult: 60 | pass 61 | 62 | 63 | def by_fn(keyfn, fn, items): 64 | """Call fn in parallel across items based on keyfn. 65 | 66 | Extensive caching/memoization is utilized when fetching data. 67 | When you run a function against tasks in a completely parallel way, the 68 | caching is skipped and there is the possibility that your endpoint will 69 | receive multiple requests. For most use cases, this significantly slows 70 | the result down (instead of speeding it up). 71 | 72 | The solution to this predicament is to execute fn in parallel but only 73 | across a specific partition function (slave ids in this example). 74 | """ 75 | 76 | # itertools.groupby returns a list of (key, generator) tuples. A job 77 | # is submitted and then the local execution context continues. The 78 | # partitioned generator is destroyed and you only end up executing fn 79 | # over a small subset of the desired partition. Therefore, the list() 80 | # conversion when submitting the partition for execution is very 81 | # important. 82 | for result in stream( 83 | lambda (k, part): [fn(i) for i in list(part)], 84 | itertools.groupby(items, keyfn)): 85 | for l in result: 86 | yield l 87 | 88 | 89 | def by_slave(fn, tasks): 90 | """Execute a function against tasks partitioned by slave.""" 91 | 92 | keyfn = lambda x: x.slave["id"] 93 | tasks = sorted(tasks, key=keyfn) 94 | return by_fn(keyfn, fn, tasks) 95 | -------------------------------------------------------------------------------- /mesos/cli/parser.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import argparse 19 | 20 | from . import completion_helpers 21 | 22 | 23 | class ArgumentParser(argparse.ArgumentParser): 24 | 25 | def enable_print_header(self): 26 | self.add_argument( 27 | '-q', action='store_true', 28 | help="Suppresses printing of headers when multiple tasks are " + 29 | "being examined" 30 | ) 31 | 32 | def task_argument(self, optional=False): 33 | kwargs = { 34 | "default": "", 35 | "type": str, 36 | "help": "ID of the task. May match multiple tasks (or all)" 37 | } 38 | if optional: 39 | kwargs["nargs"] = "?" 40 | 41 | self.add_argument('task', **kwargs).completer = completion_helpers.task 42 | 43 | def file_argument(self): 44 | self.add_argument( 45 | 'file', nargs="*", default=["stdout"], 46 | help="Path to the file inside the task's sandbox." 47 | ).completer = completion_helpers.file 48 | 49 | def path_argument(self): 50 | self.add_argument( 51 | 'path', type=str, nargs="?", default="", 52 | help="""Path to view.""" 53 | ).completer = completion_helpers.file 54 | -------------------------------------------------------------------------------- /mesos/cli/slave.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import urlparse 20 | 21 | import requests 22 | import requests.exceptions 23 | 24 | from . import exceptions, log, mesos_file, util 25 | from .cfg import CURRENT as CFG 26 | 27 | 28 | class MesosSlave(object): 29 | 30 | def __init__(self, items): 31 | self.__items = items 32 | 33 | def __getitem__(self, name): 34 | return self.__items[name] 35 | 36 | def __str__(self): 37 | return self.key() 38 | 39 | def key(self): 40 | return self["pid"].split('@')[-1] 41 | 42 | @property 43 | def host(self): 44 | return "{0}://{1}:{2}".format( 45 | CFG["scheme"], 46 | self["hostname"], 47 | self["pid"].split(":")[-1]) 48 | 49 | @log.duration 50 | def fetch(self, url, **kwargs): 51 | try: 52 | return requests.get(urlparse.urljoin( 53 | self.host, url), timeout=CFG["response_timeout"], **kwargs) 54 | except requests.exceptions.ConnectionError: 55 | raise exceptions.SlaveDoesNotExist( 56 | "Unable to connect to the slave at {0}".format(self.host)) 57 | 58 | @util.CachedProperty(ttl=5) 59 | def state(self): 60 | return self.fetch("/slave(1)/state.json").json() 61 | 62 | @property 63 | def frameworks(self): 64 | return util.merge(self.state, "frameworks", "completed_frameworks") 65 | 66 | def task_executor(self, task_id): 67 | for fw in self.frameworks: 68 | for exc in util.merge(fw, "executors", "completed_executors"): 69 | if task_id in list(map( 70 | lambda x: x["id"], 71 | util.merge( 72 | exc, "completed_tasks", "tasks", "queued_tasks"))): 73 | return exc 74 | raise exceptions.MissingExecutor("No executor has a task by that id") 75 | 76 | def file_list(self, path): 77 | # The sandbox does not exist on the slave. 78 | if path == "": 79 | return [] 80 | 81 | resp = self.fetch("/files/browse.json", params={"path": path}) 82 | if resp.status_code == 404: 83 | return [] 84 | return resp.json() 85 | 86 | def file(self, task, path): 87 | return mesos_file.File(self, task, path) 88 | 89 | @util.CachedProperty(ttl=1) 90 | def stats(self): 91 | return self.fetch("/monitor/statistics.json").json() 92 | 93 | def executor_stats(self, _id): 94 | return list(filter(lambda x: x["executor_id"])) 95 | 96 | def task_stats(self, _id): 97 | eid = self.task_executor(_id)["id"] 98 | stats = list(filter( 99 | lambda x: x["executor_id"] == eid, 100 | self.stats 101 | )) 102 | 103 | # Tasks that are not yet in a RUNNING state have no stats. 104 | if len(stats) == 0: 105 | return {} 106 | else: 107 | return stats[0]["statistics"] 108 | 109 | @property 110 | @util.memoize 111 | def log(self): 112 | return mesos_file.File(self, path="/slave/log") 113 | -------------------------------------------------------------------------------- /mesos/cli/task.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import datetime 21 | import os 22 | import re 23 | 24 | from . import exceptions, framework, mesos_file, util 25 | 26 | 27 | class Task(object): 28 | 29 | cmd_re = re.compile("\(Command: (.+)\)") 30 | 31 | def __init__(self, master, items): 32 | self.master = master 33 | self.__items = items 34 | 35 | def __str__(self): 36 | return "{0}:{1}".format(self.slave, self["id"]) 37 | 38 | def __getitem__(self, name): 39 | return self.__items[name] 40 | 41 | @property 42 | def executor(self): 43 | return self.slave.task_executor(self["id"]) 44 | 45 | @property 46 | def framework(self): 47 | return framework.Framework(self.master.framework(self["framework_id"])) 48 | 49 | @util.CachedProperty() 50 | def directory(self): 51 | try: 52 | return self.executor["directory"] 53 | except exceptions.MissingExecutor: 54 | return "" 55 | 56 | @util.CachedProperty() 57 | def slave(self): 58 | return self.master.slave(self["slave_id"]) 59 | 60 | def file(self, path): 61 | return mesos_file.File(self.slave, self, path) 62 | 63 | def file_list(self, path): 64 | return self.slave.file_list(os.path.join(self.directory, path)) 65 | 66 | @property 67 | def stats(self): 68 | try: 69 | return self.slave.task_stats(self["id"]) 70 | except exceptions.MissingExecutor: 71 | return {} 72 | 73 | @property 74 | def cpu_time(self): 75 | st = self.stats 76 | secs = st.get("cpus_user_time_secs", 0) + \ 77 | st.get("cpus_system_time_secs", 0) 78 | # timedelta has a resolution of .000000 while mesos only keeps .00 79 | return str(datetime.timedelta(seconds=secs)).rsplit(".", 1)[0] 80 | 81 | @property 82 | def cpu_limit(self): 83 | return self.stats.get("cpus_limit", 0) 84 | 85 | @property 86 | def mem_limit(self): 87 | return self.stats.get("mem_limit_bytes", 0) 88 | 89 | @property 90 | def rss(self): 91 | return self.stats.get("mem_rss_bytes", 0) 92 | 93 | @property 94 | def command(self): 95 | try: 96 | result = self.cmd_re.search(self.executor["name"]) 97 | except exceptions.MissingExecutor: 98 | result = None 99 | if not result: 100 | return "none" 101 | return result.group(1) 102 | 103 | @property 104 | def user(self): 105 | return self.framework.user 106 | -------------------------------------------------------------------------------- /mesos/cli/util.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import functools 21 | import itertools 22 | import time 23 | 24 | 25 | def merge(obj, *keys): 26 | return itertools.chain(*[obj[k] for k in keys]) 27 | 28 | 29 | def iter_until(func, pre=lambda x: False, post=lambda x: False): 30 | while 1: 31 | val = func() 32 | if pre(val): 33 | break 34 | yield val 35 | if post(val): 36 | break 37 | 38 | 39 | class CachedProperty(object): 40 | 41 | def __init__(self, ttl=300): 42 | self.ttl = ttl 43 | 44 | def __call__(self, fget, doc=None): 45 | self.fget = fget 46 | self.__doc__ = doc or fget.__doc__ 47 | self.__name__ = fget.__name__ 48 | self.__module__ = fget.__module__ 49 | return self 50 | 51 | def __get__(self, inst, owner): 52 | try: 53 | value, last_update = inst._cache[self.__name__] 54 | if self.ttl > 0 and time.time() - last_update > self.ttl: 55 | raise AttributeError 56 | except (KeyError, AttributeError): 57 | value = self.fget(inst) 58 | try: 59 | cache = inst._cache 60 | except AttributeError: 61 | cache = inst._cache = {} 62 | cache[self.__name__] = (value, time.time()) 63 | return value 64 | 65 | 66 | def memoize(obj): 67 | cache = obj.cache = {} 68 | 69 | @functools.wraps(obj) 70 | def memoizer(*args, **kwargs): 71 | key = str(args) + str(kwargs) 72 | if key not in cache: 73 | cache[key] = obj(*args, **kwargs) 74 | return cache[key] 75 | return memoizer 76 | 77 | 78 | def humanize_bytes(b): 79 | abbrevs = ( 80 | (1 << 30, 'GB'), 81 | (1 << 20, 'MB'), 82 | (1 << 10, 'kB'), 83 | (1, 'B') 84 | ) 85 | for factor, suffix in abbrevs: 86 | if b >= factor: 87 | break 88 | return '%.*f %s' % (2, b / float(factor), suffix) 89 | -------------------------------------------------------------------------------- /mesos/cli/zookeeper.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import contextlib 21 | 22 | import kazoo.client 23 | import kazoo.exceptions 24 | import kazoo.handlers.threading 25 | 26 | from . import log 27 | 28 | TIMEOUT = 1 29 | 30 | # Helper for testing 31 | client_class = kazoo.client.KazooClient 32 | 33 | 34 | @contextlib.contextmanager 35 | def client(*args, **kwargs): 36 | zk = client_class(*args, **kwargs) 37 | try: 38 | zk.start(timeout=TIMEOUT) 39 | except kazoo.handlers.threading.TimeoutError: 40 | log.fatal( 41 | "Could not connect to zookeeper. " + 42 | "Change your config via `mesos config master`") 43 | try: 44 | yield zk 45 | finally: 46 | zk.stop() 47 | zk.close() 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tox 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=1 3 | detailed-errors=1 4 | with-coverage=0 5 | with-profile=0 6 | tests=tests 7 | 8 | [isort] 9 | known_third_party=mesos.interface,google,blessings 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import imp 20 | import os 21 | import sys 22 | 23 | # Get the version without needing to install `mesos.cli`. Note that this must 24 | # be deleted from sys.modules for nosetests to run correctly. 25 | mod = imp.load_source( 26 | 'mesos.cli', 27 | os.path.join(os.path.dirname(__file__), "mesos", "cli", "__init__.py") 28 | ) 29 | version = mod.__version__ 30 | del sys.modules["mesos.cli"] 31 | 32 | with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: 33 | readme = f.read() 34 | 35 | requires = [ 36 | "argcomplete>=0.8.0", 37 | "blessings>=1.5.1", 38 | "futures>=2.1.6", 39 | "importlib>=1.0.3", # py26 40 | "kazoo>=2.0", 41 | "mesos.interface>=0.20.0", 42 | "ordereddict>=1.1", # py26 43 | "prettytable>=0.7.2", 44 | "protobuf>=2.5.0,<3", 45 | "requests>=2.3.0" 46 | ] 47 | 48 | config = { 49 | 'name': 'mesos.cli', 50 | 'version': version, 51 | 'description': 'Mesos CLI Tools', 52 | 'long_description': readme, 53 | 'author': 'Apache Mesos', 54 | 'author_email': 'dev@mesos.apache.org', 55 | 'url': 'http://pypi.python.org/pypi/mesos.cli', 56 | 'license': 'Apache 2.0', 57 | 'keywords': 'mesos', 58 | 'classifiers': [], 59 | 60 | 'namespace_packages': ['mesos'], 61 | 'packages': [ 62 | 'mesos', 63 | 'mesos.cli', 64 | 'mesos.cli.cmds' 65 | ], 66 | 'entry_points': { 67 | 'console_scripts': [ 68 | 'mesos = mesos.cli.main:main', 69 | 70 | # helpers 71 | 'mesos-completion = mesos.cli.cmds.completion:main', 72 | 'mesos-config = mesos.cli.cmds.config:main', 73 | 'mesos-resolve = mesos.cli.cmds.resolve:main', 74 | 'mesos-state = mesos.cli.cmds.state:main', 75 | 76 | # coreutils 77 | 'mesos-cat = mesos.cli.cmds.cat:main', 78 | 'mesos-events = mesos.cli.cmds.events:main', 79 | 'mesos-find = mesos.cli.cmds.find:main', 80 | 'mesos-frameworks = mesos.cli.cmds.frameworks:main', 81 | 'mesos-head = mesos.cli.cmds.head:main', 82 | 'mesos-help = mesos.cli.cmds.help:main', 83 | 'mesos-ls = mesos.cli.cmds.ls:main', 84 | 'mesos-ps = mesos.cli.cmds.ps:main', 85 | 'mesos-scp = mesos.cli.cmds.scp:main', 86 | 'mesos-ssh = mesos.cli.cmds.ssh:main', 87 | 'mesos-tail = mesos.cli.cmds.tail:main' 88 | ] 89 | }, 90 | 'install_requires': requires, 91 | 'test_suite': 'nose.collector', 92 | 'scripts': [ 93 | 'bin/mesos-zsh-completion.sh' 94 | ] 95 | } 96 | 97 | if __name__ == "__main__": 98 | from setuptools import setup 99 | 100 | setup(**config) 101 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | pass 18 | -------------------------------------------------------------------------------- /tests/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile": "test", 3 | "default": { }, 4 | "test": { 5 | "master": "zk://localhost:2181/mesos", 6 | "log_level": "debug", 7 | "log_file": "/tmp/mesos-cli.log" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/master-host: -------------------------------------------------------------------------------- 1 | zk://localhost:5050/mesos 2 | -------------------------------------------------------------------------------- /tests/data/master.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesosphere-backup/mesos-cli/c65c8bcc3087a063d437698014e30dfd165a5257/tests/data/master.pb -------------------------------------------------------------------------------- /tests/data/master_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "lost_tasks": 4, 3 | "build_user": "root", 4 | "pid": "master@10.141.141.10:5050", 5 | "build_time": 1402338541, 6 | "finished_tasks": 0, 7 | "id": "20140623-174033-177048842-5050-6160", 8 | "git_sha": "51e047524cf744ee257870eb479345646c0428ff", 9 | "build_date": "2014-06-09 18:29:01", 10 | "hostname": "mesos.vm", 11 | "version": "0.19.0", 12 | "log_dir": "/var/log/mesos", 13 | "killed_tasks": 9, 14 | "leader": "master@10.141.141.10:5050", 15 | "deactivated_slaves": 0, 16 | "failed_tasks": 0, 17 | "start_time": 1403545233.35094, 18 | "git_tag": "0.19.0", 19 | "staged_tasks": 13, 20 | "completed_frameworks": [], 21 | "elected_time": 1403545244.01246, 22 | "activated_slaves": 2, 23 | "frameworks": [ 24 | { 25 | "tasks": [ 26 | { 27 | "executor_id": "", 28 | "name": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 29 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 30 | "state": "TASK_RUNNING", 31 | "statuses": [ 32 | { 33 | "timestamp": 1403736266.31089, 34 | "state": "TASK_RUNNING" 35 | } 36 | ], 37 | "slave_id": "20140619-151434-16842879-5050-1196-0", 38 | "id": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 39 | "resources": { 40 | "mem": 16, 41 | "disk": 0, 42 | "cpus": 0.1, 43 | "ports": "[31654-31654]" 44 | } 45 | }, 46 | { 47 | "executor_id": "", 48 | "name": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 49 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 50 | "state": "TASK_RUNNING", 51 | "statuses": [ 52 | { 53 | "timestamp": 1403736269.35222, 54 | "state": "TASK_RUNNING" 55 | } 56 | ], 57 | "slave_id": "20140619-151434-16842879-5050-1196-0", 58 | "id": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 59 | "resources": { 60 | "mem": 16, 61 | "disk": 0, 62 | "cpus": 0.1, 63 | "ports": "[31705-31705]" 64 | } 65 | } 66 | ], 67 | "name": "marathon-0.6.0-SNAPSHOT", 68 | "hostname": "mesos.vm", 69 | "checkpoint": false, 70 | "offers": [], 71 | "failover_timeout": 604800, 72 | "completed_tasks": [ 73 | { 74 | "executor_id": "", 75 | "name": "app-15.2b6891a2-fafd-11e3-a955-b6f6cc110ef2", 76 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 77 | "state": "TASK_KILLED", 78 | "statuses": [ 79 | { 80 | "timestamp": 1403545102.37249, 81 | "state": "TASK_RUNNING" 82 | }, 83 | { 84 | "timestamp": 1403732313.32661, 85 | "state": "TASK_KILLED" 86 | } 87 | ], 88 | "slave_id": "20140619-151434-16842879-5050-1196-0", 89 | "id": "app-15.2b6891a2-fafd-11e3-a955-b6f6cc110ef2", 90 | "resources": { 91 | "mem": 16, 92 | "disk": 0, 93 | "cpus": 0.1, 94 | "ports": "[31768-31768]" 95 | } 96 | }, 97 | { 98 | "executor_id": "", 99 | "name": "app-15.d4077961-fcb0-11e3-8b67-b6f6cc110ef2", 100 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 101 | "state": "TASK_KILLED", 102 | "statuses": [ 103 | { 104 | "timestamp": 1403732216.32838, 105 | "state": "TASK_RUNNING" 106 | }, 107 | { 108 | "timestamp": 1403732313.44489, 109 | "state": "TASK_KILLED" 110 | } 111 | ], 112 | "slave_id": "20140619-151434-16842879-5050-1196-0", 113 | "id": "app-15.d4077961-fcb0-11e3-8b67-b6f6cc110ef2", 114 | "resources": { 115 | "mem": 16, 116 | "disk": 0, 117 | "cpus": 0.1, 118 | "ports": "[31650-31650]" 119 | } 120 | }, 121 | { 122 | "executor_id": "", 123 | "name": "app-15.d793d472-fcb0-11e3-8b67-b6f6cc110ef2", 124 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 125 | "state": "TASK_KILLED", 126 | "statuses": [ 127 | { 128 | "timestamp": 1403732221.33185, 129 | "state": "TASK_RUNNING" 130 | }, 131 | { 132 | "timestamp": 1403732313.44429, 133 | "state": "TASK_KILLED" 134 | } 135 | ], 136 | "slave_id": "20140619-151434-16842879-5050-1196-0", 137 | "id": "app-15.d793d472-fcb0-11e3-8b67-b6f6cc110ef2", 138 | "resources": { 139 | "mem": 16, 140 | "disk": 0, 141 | "cpus": 0.1, 142 | "ports": "[31974-31974]" 143 | } 144 | }, 145 | { 146 | "executor_id": "", 147 | "name": "app-15.db275b73-fcb0-11e3-8b67-b6f6cc110ef2", 148 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 149 | "state": "TASK_KILLED", 150 | "statuses": [ 151 | { 152 | "timestamp": 1403732227.3805, 153 | "state": "TASK_RUNNING" 154 | }, 155 | { 156 | "timestamp": 1403732313.46733, 157 | "state": "TASK_KILLED" 158 | } 159 | ], 160 | "slave_id": "20140619-151434-16842879-5050-1196-0", 161 | "id": "app-15.db275b73-fcb0-11e3-8b67-b6f6cc110ef2", 162 | "resources": { 163 | "mem": 16, 164 | "disk": 0, 165 | "cpus": 0.1, 166 | "ports": "[31651-31651]" 167 | } 168 | }, 169 | { 170 | "executor_id": "", 171 | "name": "app-15.e54e8a16-fcb0-11e3-8b67-b6f6cc110ef2", 172 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 173 | "state": "TASK_KILLED", 174 | "statuses": [ 175 | { 176 | "timestamp": 1403732244.43668, 177 | "state": "TASK_RUNNING" 178 | }, 179 | { 180 | "timestamp": 1403732313.50248, 181 | "state": "TASK_KILLED" 182 | } 183 | ], 184 | "slave_id": "20140619-151434-16842879-5050-1196-0", 185 | "id": "app-15.e54e8a16-fcb0-11e3-8b67-b6f6cc110ef2", 186 | "resources": { 187 | "mem": 16, 188 | "disk": 0, 189 | "cpus": 0.1, 190 | "ports": "[31995-31995]" 191 | } 192 | }, 193 | { 194 | "executor_id": "", 195 | "name": "app-15.debbf3e4-fcb0-11e3-8b67-b6f6cc110ef2", 196 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 197 | "state": "TASK_KILLED", 198 | "statuses": [ 199 | { 200 | "timestamp": 1403732233.40399, 201 | "state": "TASK_RUNNING" 202 | }, 203 | { 204 | "timestamp": 1403732313.50099, 205 | "state": "TASK_KILLED" 206 | } 207 | ], 208 | "slave_id": "20140619-151434-16842879-5050-1196-0", 209 | "id": "app-15.debbf3e4-fcb0-11e3-8b67-b6f6cc110ef2", 210 | "resources": { 211 | "mem": 16, 212 | "disk": 0, 213 | "cpus": 0.1, 214 | "ports": "[31637-31637]" 215 | } 216 | }, 217 | { 218 | "executor_id": "", 219 | "name": "app-15.e2526115-fcb0-11e3-8b67-b6f6cc110ef2", 220 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 221 | "state": "TASK_KILLED", 222 | "statuses": [ 223 | { 224 | "timestamp": 1403732239.42299, 225 | "state": "TASK_RUNNING" 226 | }, 227 | { 228 | "timestamp": 1403732313.49919, 229 | "state": "TASK_KILLED" 230 | } 231 | ], 232 | "slave_id": "20140619-151434-16842879-5050-1196-0", 233 | "id": "app-15.e2526115-fcb0-11e3-8b67-b6f6cc110ef2", 234 | "resources": { 235 | "mem": 16, 236 | "disk": 0, 237 | "cpus": 0.1, 238 | "ports": "[31939-31939]" 239 | } 240 | }, 241 | { 242 | "executor_id": "", 243 | "name": "app-15.ec771eb8-fcb0-11e3-8b67-b6f6cc110ef2", 244 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 245 | "state": "TASK_KILLED", 246 | "statuses": [ 247 | { 248 | "timestamp": 1403732256.48262, 249 | "state": "TASK_RUNNING" 250 | }, 251 | { 252 | "timestamp": 1403732313.56178, 253 | "state": "TASK_KILLED" 254 | } 255 | ], 256 | "slave_id": "20140619-151434-16842879-5050-1196-0", 257 | "id": "app-15.ec771eb8-fcb0-11e3-8b67-b6f6cc110ef2", 258 | "resources": { 259 | "mem": 16, 260 | "disk": 0, 261 | "cpus": 0.1, 262 | "ports": "[31943-31943]" 263 | } 264 | }, 265 | { 266 | "executor_id": "", 267 | "name": "app-15.ef7a9ab9-fcb0-11e3-8b67-b6f6cc110ef2", 268 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 269 | "state": "TASK_KILLED", 270 | "statuses": [ 271 | { 272 | "timestamp": 1403732261.48864, 273 | "state": "TASK_RUNNING" 274 | }, 275 | { 276 | "timestamp": 1403732313.55922, 277 | "state": "TASK_KILLED" 278 | } 279 | ], 280 | "slave_id": "20140619-151434-16842879-5050-1196-0", 281 | "id": "app-15.ef7a9ab9-fcb0-11e3-8b67-b6f6cc110ef2", 282 | "resources": { 283 | "mem": 16, 284 | "disk": 0, 285 | "cpus": 0.1, 286 | "ports": "[31647-31647]" 287 | } 288 | }, 289 | { 290 | "executor_id": "", 291 | "name": "app-15.e8e2fb77-fcb0-11e3-8b67-b6f6cc110ef2", 292 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 293 | "state": "TASK_LOST", 294 | "statuses": [ 295 | { 296 | "timestamp": 1403732250.45604, 297 | "state": "TASK_RUNNING" 298 | }, 299 | { 300 | "timestamp": 1403736220.1014, 301 | "state": "TASK_LOST" 302 | } 303 | ], 304 | "slave_id": "20140619-151434-16842879-5050-1196-0", 305 | "id": "app-15.e8e2fb77-fcb0-11e3-8b67-b6f6cc110ef2", 306 | "resources": { 307 | "mem": 16, 308 | "disk": 0, 309 | "cpus": 0.1, 310 | "ports": "[31646-31646]" 311 | } 312 | }, 313 | { 314 | "executor_id": "", 315 | "name": "app-215.2e6508c3-fafd-11e3-a955-b6f6cc110ef2", 316 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 317 | "state": "TASK_LOST", 318 | "statuses": [ 319 | { 320 | "timestamp": 1403545125.42471, 321 | "state": "TASK_RUNNING" 322 | }, 323 | { 324 | "timestamp": 1403736220.10378, 325 | "state": "TASK_LOST" 326 | } 327 | ], 328 | "slave_id": "20140619-151434-16842879-5050-1196-0", 329 | "id": "app-215.2e6508c3-fafd-11e3-a955-b6f6cc110ef2", 330 | "resources": { 331 | "mem": 16, 332 | "disk": 0, 333 | "cpus": 0.1, 334 | "ports": "[31964-31964]" 335 | } 336 | }, 337 | { 338 | "executor_id": "", 339 | "name": "app-215.2a1d811b-fcba-11e3-8b67-b6f6cc110ef2", 340 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 341 | "state": "TASK_LOST", 342 | "statuses": [ 343 | { 344 | "timestamp": 1403736238.28576, 345 | "state": "TASK_RUNNING" 346 | }, 347 | { 348 | "timestamp": 1403736245.2927, 349 | "state": "TASK_LOST" 350 | } 351 | ], 352 | "slave_id": "20140619-151434-16842879-5050-1196-0", 353 | "id": "app-215.2a1d811b-fcba-11e3-8b67-b6f6cc110ef2", 354 | "resources": { 355 | "mem": 16, 356 | "disk": 0, 357 | "cpus": 0.1, 358 | "ports": "[31175-31175]" 359 | } 360 | }, 361 | { 362 | "executor_id": "", 363 | "name": "app-15.27217f2a-fcba-11e3-8b67-b6f6cc110ef2", 364 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 365 | "state": "TASK_LOST", 366 | "statuses": [ 367 | { 368 | "timestamp": 1403736221.23526, 369 | "state": "TASK_RUNNING" 370 | }, 371 | { 372 | "timestamp": 1403736245.29414, 373 | "state": "TASK_LOST" 374 | } 375 | ], 376 | "slave_id": "20140619-151434-16842879-5050-1196-0", 377 | "id": "app-15.27217f2a-fcba-11e3-8b67-b6f6cc110ef2", 378 | "resources": { 379 | "mem": 16, 380 | "disk": 0, 381 | "cpus": 0.1, 382 | "ports": "[31714-31714]" 383 | } 384 | } 385 | ], 386 | "role": "*", 387 | "user": "root", 388 | "active": true, 389 | "unregistered_time": 0, 390 | "registered_time": 1403545244.06318, 391 | "reregistered_time": 1403732199.84529, 392 | "id": "20140612-230025-16842879-5050-1151-0000", 393 | "resources": { 394 | "mem": 32, 395 | "disk": 0, 396 | "cpus": 0.2 397 | } 398 | } 399 | ], 400 | "flags": { 401 | "help": "false", 402 | "zk": "zk://localhost:2181/mesos", 403 | "whitelist": "*", 404 | "recovery_slave_removal_limit": "100%", 405 | "port": "5050", 406 | "logbufsecs": "0", 407 | "authenticate": "false", 408 | "work_dir": "/var/lib/mesos", 409 | "slave_reregister_timeout": "10mins", 410 | "authenticate_slaves": "false", 411 | "framework_sorter": "drf", 412 | "version": "false", 413 | "log_dir": "/var/log/mesos", 414 | "logging_level": "INFO", 415 | "log_auto_initialize": "true", 416 | "registry_strict": "false", 417 | "registry_fetch_timeout": "1mins", 418 | "root_submissions": "true", 419 | "webui_dir": "/usr/local/share/mesos/webui", 420 | "registry": "replicated_log", 421 | "allocation_interval": "1secs", 422 | "zk_session_timeout": "10secs", 423 | "quorum": "1", 424 | "user_sorter": "drf", 425 | "quiet": "false", 426 | "registry_store_timeout": "5secs" 427 | }, 428 | "started_tasks": 0, 429 | "slaves": [ 430 | { 431 | "hostname": "10.141.141.10", 432 | "pid": "slave(1)@10.141.141.10:5052", 433 | "attributes": {}, 434 | "registered_time": 1403736321.15912, 435 | "id": "20140623-174033-177048842-5050-6160-0", 436 | "resources": { 437 | "mem": 1000, 438 | "disk": 34068, 439 | "cpus": 2, 440 | "ports": "[31000-32000]" 441 | } 442 | }, 443 | { 444 | "hostname": "10.141.141.10", 445 | "pid": "slave(1)@10.141.141.10:5051", 446 | "attributes": {}, 447 | "registered_time": 1403545244.90016, 448 | "reregistered_time": 1403736259.18602, 449 | "id": "20140619-151434-16842879-5050-1196-0", 450 | "resources": { 451 | "mem": 1000, 452 | "disk": 34068, 453 | "cpus": 2, 454 | "ports": "[31000-32000]" 455 | } 456 | } 457 | ] 458 | } 459 | -------------------------------------------------------------------------------- /tests/data/sandbox/foo/bar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesosphere-backup/mesos-cli/c65c8bcc3087a063d437698014e30dfd165a5257/tests/data/sandbox/foo/bar -------------------------------------------------------------------------------- /tests/data/sandbox/log: -------------------------------------------------------------------------------- 1 | foobar 2 | 3 | test1234 4 | -------------------------------------------------------------------------------- /tests/data/sandbox/stderr: -------------------------------------------------------------------------------- 1 | WARNING: Logging before InitGoogleLogging() is written to STDERR 2 | I0625 22:44:19.287855 29509 fetcher.cpp:73] Fetching URI 'http://twistedmatrix.com/Releases/Twisted/14.0/Twisted-14.0.0.tar.bz2' 3 | I0625 22:44:19.288079 29509 fetcher.cpp:123] Downloading 'http://twistedmatrix.com/Releases/Twisted/14.0/Twisted-14.0.0.tar.bz2' to '/tmp/mesos/slaves/20140619-151434-16842879-5050-1196-0/frameworks/20140612-230025-16842879-5050-1151-0000/executors/app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2/runs/3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba/Twisted-14.0.0.tar.bz2' 4 | I0625 22:44:29.127799 29509 fetcher.cpp:61] Extracted resource '/tmp/mesos/slaves/20140619-151434-16842879-5050-1196-0/frameworks/20140612-230025-16842879-5050-1151-0000/executors/app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2/runs/3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba/Twisted-14.0.0.tar.bz2' into '/tmp/mesos/slaves/20140619-151434-16842879-5050-1196-0/frameworks/20140612-230025-16842879-5050-1151-0000/executors/app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2/runs/3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba' 5 | WARNING: Logging before InitGoogleLogging() is written to STDERR 6 | I0625 22:44:29.337065 29542 exec.cpp:131] Version: 0.19.0 7 | I0625 22:44:29.344285 29551 exec.cpp:205] Executor registered on slave 20140619-151434-16842879-5050-1196-0 8 | -------------------------------------------------------------------------------- /tests/data/sandbox/stdout: -------------------------------------------------------------------------------- 1 | Registered executor on 10.141.141.10 2 | Starting task app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2 3 | sh -c 'while true; do sleep 10; done' 4 | Forked command at 29554 5 | -------------------------------------------------------------------------------- /tests/data/slave-20140619-151434-16842879-5050-1196-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": {}, 3 | "started_tasks": 0, 4 | "flags": { 5 | "recovery_timeout": "15mins", 6 | "cgroups_root": "mesos", 7 | "quiet": "false", 8 | "isolation": "posix/cpu,posix/mem", 9 | "default_role": "*", 10 | "executor_shutdown_grace_period": "5secs", 11 | "gc_delay": "1weeks", 12 | "frameworks_home": "", 13 | "resource_monitoring_interval": "1secs", 14 | "logging_level": "INFO", 15 | "hadoop_home": "", 16 | "log_dir": "/var/log/mesos", 17 | "master": "zk://localhost:2181/mesos", 18 | "cgroups_hierarchy": "/sys/fs/cgroup", 19 | "help": "false", 20 | "port": "5051", 21 | "executor_registration_timeout": "1mins", 22 | "recover": "reconnect", 23 | "launcher_dir": "/usr/local/libexec/mesos", 24 | "cgroups_enable_cfs": "false", 25 | "logbufsecs": "0", 26 | "work_dir": "/tmp/mesos", 27 | "switch_user": "true", 28 | "disk_watch_interval": "1mins", 29 | "hostname": "10.141.141.10", 30 | "registration_backoff_factor": "1secs", 31 | "checkpoint": "true", 32 | "strict": "true", 33 | "version": "false" 34 | }, 35 | "frameworks": [ 36 | { 37 | "id": "20140612-230025-16842879-5050-1151-0000", 38 | "name": "marathon-0.6.0-SNAPSHOT", 39 | "executors": [ 40 | { 41 | "resources": { 42 | "ports": "[31654-31654]", 43 | "cpus": 0.1, 44 | "disk": 0, 45 | "mem": 16 46 | }, 47 | "tasks": [ 48 | { 49 | "resources": { 50 | "ports": "[31654-31654]", 51 | "cpus": 0.1, 52 | "disk": 0, 53 | "mem": 16 54 | }, 55 | "id": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 56 | "slave_id": "20140619-151434-16842879-5050-1196-0", 57 | "statuses": [ 58 | { 59 | "state": "TASK_RUNNING", 60 | "timestamp": 1403736266.31089 61 | } 62 | ], 63 | "state": "TASK_RUNNING", 64 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 65 | "name": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 66 | "executor_id": "" 67 | } 68 | ], 69 | "container": "6de5ce8a-62c2-473a-9523-ee0fad1f0d5c", 70 | "name": "Command Executor (Task: app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2) (Command: sh -c 'while true; ...')", 71 | "source": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 72 | "queued_tasks": [], 73 | "completed_tasks": [], 74 | "directory": "/tmp/mesos/slaves/20140619-151434-16842879-5050-1196-0/frameworks/20140612-230025-16842879-5050-1151-0000/executors/app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2/runs/6de5ce8a-62c2-473a-9523-ee0fad1f0d5c", 75 | "id": "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2" 76 | }, 77 | { 78 | "resources": { 79 | "ports": "[31705-31705]", 80 | "cpus": 0.1, 81 | "disk": 0, 82 | "mem": 16 83 | }, 84 | "tasks": [ 85 | { 86 | "resources": { 87 | "ports": "[31705-31705]", 88 | "cpus": 0.1, 89 | "disk": 0, 90 | "mem": 16 91 | }, 92 | "id": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 93 | "slave_id": "20140619-151434-16842879-5050-1196-0", 94 | "statuses": [ 95 | { 96 | "state": "TASK_RUNNING", 97 | "timestamp": 1403736269.35222 98 | } 99 | ], 100 | "state": "TASK_RUNNING", 101 | "framework_id": "20140612-230025-16842879-5050-1151-0000", 102 | "name": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 103 | "executor_id": "" 104 | } 105 | ], 106 | "container": "3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba", 107 | "name": "Command Executor (Task: app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2) (Command: sh -c 'while true; ...')", 108 | "source": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 109 | "queued_tasks": [], 110 | "completed_tasks": [], 111 | "directory": "/tmp/mesos/slaves/20140619-151434-16842879-5050-1196-0/frameworks/20140612-230025-16842879-5050-1151-0000/executors/app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2/runs/3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba", 112 | "id": "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 113 | } 114 | ], 115 | "hostname": "mesos.vm", 116 | "checkpoint": false, 117 | "failover_timeout": 604800, 118 | "completed_executors": [], 119 | "role": "*", 120 | "user": "root" 121 | } 122 | ], 123 | "completed_frameworks": [], 124 | "staged_tasks": 4, 125 | "git_tag": "0.19.0", 126 | "build_date": "2014-06-09 18:29:01", 127 | "git_sha": "51e047524cf744ee257870eb479345646c0428ff", 128 | "id": "20140619-151434-16842879-5050-1196-0", 129 | "finished_tasks": 0, 130 | "build_time": 1402338541, 131 | "pid": "slave(1)@10.141.141.10:5051", 132 | "build_user": "root", 133 | "lost_tasks": 0, 134 | "hostname": "10.141.141.10", 135 | "version": "0.19.0", 136 | "log_dir": "/var/log/mesos", 137 | "killed_tasks": 0, 138 | "master_hostname": "mesos.vm", 139 | "resources": { 140 | "cpus": 0, 141 | "disk": 0, 142 | "mem": 0 143 | }, 144 | "failed_tasks": 0, 145 | "start_time": 1403736258.8475 146 | } 147 | -------------------------------------------------------------------------------- /tests/data/slave_statistics.json: -------------------------------------------------------------------------------- 1 | [{"executor_id":"app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2","executor_name":"Command Executor (Task: app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2) (Command: sh -c 'while true; ...')","framework_id":"20140612-230025-16842879-5050-1151-0000","source":"app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2","statistics":{"cpus_limit":0.1,"cpus_system_time_secs":55.99,"cpus_user_time_secs":26.25,"mem_limit_bytes":16777216,"mem_rss_bytes":10964992,"timestamp":1403819237.307}},{"executor_id":"app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2","executor_name":"Command Executor (Task: app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2) (Command: sh -c 'while true; ...')","framework_id":"20140612-230025-16842879-5050-1151-0000","source":"app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2","statistics":{"cpus_limit":0.1,"cpus_system_time_secs":56.97,"cpus_user_time_secs":26.36,"mem_limit_bytes":16777216,"mem_rss_bytes":10973184,"timestamp":1403819237.32698}}] -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | pass 18 | -------------------------------------------------------------------------------- /tests/integration/test_cat.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.cat 23 | import mesos.cli.exceptions 24 | 25 | from .. import utils 26 | 27 | 28 | @mock.patch("mesos.cli.mesos_file.File._fetch", utils.sandbox_read) 29 | class TestCat(utils.MockState): 30 | 31 | @utils.patch_args([ 32 | "mesos-cat", 33 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 34 | ]) 35 | def test_single_default(self): 36 | mesos.cli.cmds.cat.main() 37 | 38 | assert len(self.lines) == 5 39 | 40 | @utils.patch_args([ 41 | "mesos-cat", 42 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 43 | "stderr" 44 | ]) 45 | def test_single_specific(self): 46 | mesos.cli.cmds.cat.main() 47 | 48 | assert len(self.lines) == 8 49 | 50 | @utils.patch_args([ 51 | "mesos-cat", 52 | "app", 53 | "st" 54 | ]) 55 | def test_partial(self): 56 | self.assertRaises(SystemExit, mesos.cli.cmds.cat.main) 57 | 58 | assert "No such task" in self.stdout 59 | 60 | @utils.patch_args([ 61 | "mesos-cat", 62 | "app" 63 | ]) 64 | def test_multiple_tasks(self): 65 | mesos.cli.cmds.cat.main() 66 | 67 | assert len(self.lines) == 9 68 | 69 | @utils.patch_args([ 70 | "mesos-cat", 71 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 72 | "stdout", 73 | "stderr" 74 | ]) 75 | def test_multiple_files(self): 76 | mesos.cli.cmds.cat.main() 77 | 78 | assert len(self.lines) == 12 79 | 80 | @utils.patch_args([ 81 | "mesos-cat", 82 | "app-215.2a1d811b-fcba-11e3-8b67-b6f6cc110ef2", 83 | "stdout" 84 | ]) 85 | def test_missing(self): 86 | self.assertRaises(SystemExit, mesos.cli.cmds.cat.main) 87 | 88 | assert "No such task" in self.stdout 89 | -------------------------------------------------------------------------------- /tests/integration/test_completion.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | import sys 22 | 23 | import mock 24 | 25 | import mesos.cli.cmds.completion 26 | 27 | from .. import utils 28 | 29 | 30 | def generate_env(line): 31 | env = { 32 | "COMP_LINE": line, 33 | "COMP_POINT": len(line) 34 | } 35 | env.update(os.environ) 36 | return env 37 | 38 | 39 | # There are some side effects in completion. To test completers, make sure you 40 | # use different commands. Otherwise, you'll get what appears to be random 41 | # failures. 42 | @mock.patch("mesos.cli.cmds.completion.EXIT", sys.exit) 43 | class TestCompletion(utils.MockState): 44 | 45 | @mock.patch("os.environ", generate_env("mesos ")) 46 | @mock.patch("mesos.cli.cli.cmds", lambda **kwargs: ["help"]) 47 | def test_cmds(self): 48 | mesos.cli.cmds.completion.main() 49 | 50 | assert "help" in self.stdout 51 | 52 | @mock.patch("os.environ", generate_env("mesos cat ")) 53 | def test_task(self): 54 | self.assertRaises(SystemExit, mesos.cli.cmds.completion.main) 55 | 56 | assert "app-15" in self.stdout 57 | assert "app-215" in self.stdout 58 | 59 | reload(mesos.cli.cmds.cat) 60 | 61 | @mock.patch("os.environ", generate_env("mesos state 2")) 62 | def test_slave(self): 63 | self.assertRaises(SystemExit, mesos.cli.cmds.completion.main) 64 | 65 | assert len(self.stdout.split("\n")) == 2 66 | 67 | reload(mesos.cli.cmds.state) 68 | 69 | @mock.patch("os.environ", generate_env("mesos ls app-215 Twisted-14.0.0/")) 70 | @mock.patch("mesos.cli.slave.MesosSlave.file_list", utils.file_list) 71 | def test_file(self): 72 | self.assertRaises(SystemExit, mesos.cli.cmds.completion.main) 73 | 74 | assert "twisted/" in self.stdout 75 | assert "NEWS" in self.stdout 76 | 77 | reload(mesos.cli.cmds.ls) 78 | -------------------------------------------------------------------------------- /tests/integration/test_config.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import json 20 | import os 21 | import tempfile 22 | 23 | import mock 24 | 25 | import mesos.cli.cfg 26 | import mesos.cli.cmds.config 27 | 28 | from .. import utils 29 | 30 | config_path = os.path.normpath(os.path.join( 31 | os.path.dirname(__file__), "..", "data", "config.json")) 32 | 33 | 34 | class TestConfig(utils.MockState): 35 | 36 | @mock.patch('os.environ', {"MESOS_CLI_CONFIG": config_path}) 37 | @utils.patch_args(["mesos-config"]) 38 | def test_output(self): 39 | mesos.cli.cmds.config.CFG = mesos.cli.cfg.Config() 40 | 41 | mesos.cli.cmds.config.main() 42 | 43 | out = json.loads(self.stdout) 44 | assert "master" in out["test"] 45 | 46 | @mock.patch('os.environ', {"MESOS_CLI_CONFIG": config_path}) 47 | @utils.patch_args([ 48 | "mesos-config", 49 | "master" 50 | ]) 51 | def test_get_key(self): 52 | mesos.cli.cmds.config.CFG = mesos.cli.cfg.Config() 53 | 54 | mesos.cli.cmds.config.main() 55 | 56 | assert "zk://localhost:2181/mesos" in self.stdout 57 | 58 | @utils.patch_args([ 59 | "mesos-config", 60 | "master", 61 | "zk://localhost:2181/mesos" 62 | ]) 63 | def test_set_key(self): 64 | fd, fname = tempfile.mkstemp() 65 | with open(fname, "w") as fobj: 66 | fobj.write("{}") 67 | try: 68 | with mock.patch('os.environ', {"MESOS_CLI_CONFIG": fname}): 69 | mesos.cli.cmds.config.CFG = mesos.cli.cfg.Config() 70 | 71 | mesos.cli.cmds.config.main() 72 | 73 | with open(fname, "r") as fobj: 74 | assert "zk://localhost:2181" in json.loads( 75 | fobj.read())["default"]["master"] 76 | finally: 77 | os.remove(fname) 78 | -------------------------------------------------------------------------------- /tests/integration/test_events.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | import re 22 | 23 | import mock 24 | 25 | import mesos.cli.cmds.events 26 | 27 | from .. import utils 28 | 29 | 30 | @mock.patch("mesos.cli.mesos_file.File._fetch", utils.sandbox_read) 31 | class TestEvents(utils.MockState): 32 | 33 | @utils.patch_args([ 34 | "mesos-events", 35 | "--sleep-interval=0.1" 36 | ]) 37 | @mock.patch("mesos.cli.cmds.events.FOLLOW", False) 38 | @mock.patch("mesos.cli.cmds.events.POSITION", os.SEEK_SET) 39 | def test_stream(self): 40 | mesos.cli.cmds.events.main() 41 | 42 | assert len(re.findall("/slave/log", self.stdout)) == 2 43 | assert len(re.findall("/master/log", self.stdout)) == 1 44 | -------------------------------------------------------------------------------- /tests/integration/test_find.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.find 23 | 24 | from .. import utils 25 | 26 | 27 | @mock.patch("mesos.cli.slave.MesosSlave.file_list", utils.file_list) 28 | class TestFind(utils.MockState): 29 | 30 | @utils.patch_args([ 31 | "mesos-find", 32 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2" 33 | ]) 34 | def test_single(self): 35 | mesos.cli.cmds.find.main() 36 | 37 | assert len(self.lines) == 3 38 | 39 | @utils.patch_args([ 40 | "mesos-find", 41 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 42 | "Twisted-14.0.0/twisted/words/xish/" 43 | ]) 44 | def test_path(self): 45 | mesos.cli.cmds.find.main() 46 | 47 | assert len(self.lines) == 8 48 | 49 | @utils.patch_args([ 50 | "mesos-find", 51 | "app" 52 | ]) 53 | def test_multiple_tasks(self): 54 | mesos.cli.cmds.find.main() 55 | 56 | assert len(self.lines) == 1872 57 | 58 | @utils.patch_args([ 59 | "mesos-find", 60 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 61 | "std" 62 | ]) 63 | def test_partial(self): 64 | mesos.cli.cmds.find.main() 65 | 66 | assert len(self.stdout) == 0 67 | 68 | @utils.patch_args([ 69 | "mesos-find", 70 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 71 | "stdout" 72 | ]) 73 | def test_exact(self): 74 | mesos.cli.cmds.find.main() 75 | 76 | assert len(self.stdout) == 0 77 | 78 | @utils.patch_args([ 79 | "mesos-find", 80 | "-q", 81 | "app", 82 | "std" 83 | ]) 84 | def test_hide_header(self): 85 | mesos.cli.cmds.find.main() 86 | 87 | assert len(self.stdout) == 0 88 | 89 | @utils.patch_args([ 90 | "mesos-find", 91 | "app", 92 | "std" 93 | ]) 94 | def test_empty_files(self): 95 | mesos.cli.cmds.find.main() 96 | 97 | assert len(self.lines) == 16 98 | -------------------------------------------------------------------------------- /tests/integration/test_head.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.head 23 | 24 | from .. import utils 25 | 26 | 27 | @mock.patch("mesos.cli.mesos_file.File._fetch", utils.sandbox_read) 28 | class TestHead(utils.MockState): 29 | 30 | @utils.patch_args([ 31 | "mesos-head", 32 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 33 | ]) 34 | def test_single_default(self): 35 | mesos.cli.cmds.head.main() 36 | 37 | assert len(self.lines) == 6 38 | 39 | @utils.patch_args([ 40 | "mesos-head", 41 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 42 | "stderr" 43 | ]) 44 | def test_single_specific(self): 45 | mesos.cli.cmds.head.main() 46 | 47 | assert len(self.lines) == 9 48 | 49 | @utils.patch_args([ 50 | "mesos-head", 51 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 52 | "st" 53 | ]) 54 | def test_partial(self): 55 | self.assertRaises(SystemExit, mesos.cli.cmds.head.main) 56 | 57 | assert len(self.lines) == 2 58 | 59 | @utils.patch_args([ 60 | "mesos-head", 61 | "app" 62 | ]) 63 | def test_multiple_tasks(self): 64 | mesos.cli.cmds.head.main() 65 | 66 | assert len(self.lines) == 11 67 | 68 | @utils.patch_args([ 69 | "mesos-head", 70 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 71 | "stdout", 72 | "stderr" 73 | ]) 74 | def test_multiple_files(self): 75 | mesos.cli.cmds.head.main() 76 | 77 | assert len(self.lines) == 14 78 | 79 | @utils.patch_args([ 80 | "mesos-head", 81 | "-n", "1", 82 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 83 | ]) 84 | def test_line_limit(self): 85 | mesos.cli.cmds.head.main() 86 | 87 | assert "Registered" in self.stdout 88 | assert len(self.lines) == 3 89 | 90 | @utils.patch_args([ 91 | "mesos-head", 92 | "-q", 93 | "app" 94 | ]) 95 | def test_hide_header(self): 96 | mesos.cli.cmds.head.main() 97 | 98 | assert len(self.lines) == 9 99 | -------------------------------------------------------------------------------- /tests/integration/test_ls.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.ls 23 | 24 | from .. import utils 25 | 26 | 27 | @mock.patch("mesos.cli.slave.MesosSlave.file_list", utils.file_list) 28 | class TestLs(utils.MockState): 29 | 30 | @utils.patch_args([ 31 | "mesos-ls", 32 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2" 33 | ]) 34 | def test_single(self): 35 | mesos.cli.cmds.ls.main() 36 | 37 | # mode 38 | assert "-rw-r--r-x" in self.stdout 39 | # user + group 40 | assert "root root" in self.stdout 41 | # size 42 | assert "231" in self.stdout 43 | # date 44 | assert "Jun 25 15:44" in self.stdout 45 | # name 46 | assert "stdout" in self.stdout 47 | 48 | assert len(self.lines) == 3 49 | 50 | @utils.patch_args([ 51 | "mesos-ls", 52 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 53 | "Twisted-14.0.0/" 54 | ]) 55 | def test_path(self): 56 | mesos.cli.cmds.ls.main() 57 | 58 | assert len(self.lines) == 12 59 | 60 | @utils.patch_args([ 61 | "mesos-ls", 62 | "app" 63 | ]) 64 | def test_multiple_tasks(self): 65 | mesos.cli.cmds.ls.main() 66 | 67 | assert len(self.lines) == 22 68 | 69 | @utils.patch_args([ 70 | "mesos-ls", 71 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 72 | "std" 73 | ]) 74 | def test_partial(self): 75 | mesos.cli.cmds.ls.main() 76 | 77 | assert len(self.stdout) == 0 78 | 79 | @utils.patch_args([ 80 | "mesos-ls", 81 | "app-15.41fef02d-fcba-11e3-8b67-b6f6cc110ef2", 82 | "stdout" 83 | ]) 84 | def test_exact(self): 85 | mesos.cli.cmds.ls.main() 86 | 87 | assert len(self.stdout) == 0 88 | 89 | @utils.patch_args([ 90 | "mesos-ls", 91 | "-q", 92 | "app", 93 | "std" 94 | ]) 95 | def test_hide_header(self): 96 | mesos.cli.cmds.ls.main() 97 | 98 | assert len(self.stdout) == 0 99 | 100 | @utils.patch_args([ 101 | "mesos-ls", 102 | "app", 103 | "std" 104 | ]) 105 | def test_empty_files(self): 106 | mesos.cli.cmds.ls.main() 107 | 108 | assert len(self.lines) == 16 109 | -------------------------------------------------------------------------------- /tests/integration/test_ps.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.ps 23 | 24 | from .. import utils 25 | 26 | 27 | @mock.patch("mesos.cli.slave.MesosSlave.stats", utils.slave_stats) 28 | class TestPs(utils.MockState): 29 | 30 | @utils.patch_args(["mesos-ps"]) 31 | def test_format(self): 32 | mesos.cli.cmds.ps.main() 33 | 34 | # Time 35 | assert "0:01:23" in self.stdout 36 | # RSS 37 | assert "10.46 MB" in self.stdout 38 | # CPU 39 | assert "0.1" in self.stdout 40 | # MEM 41 | assert "65.41" in self.stdout 42 | # Command 43 | assert "while true" in self.stdout 44 | # User 45 | assert "root" in self.stdout 46 | # PID 47 | assert "app-215.3e" in self.stdout 48 | 49 | assert len(self.lines) == 4 50 | 51 | @utils.patch_args(["mesos-ps", "-i"]) 52 | def test_inactive(self): 53 | mesos.cli.cmds.ps.main() 54 | 55 | assert len(self.lines) == 17 56 | 57 | @utils.patch_args(["mesos-ps", "foo"]) 58 | def test_filter(self): 59 | mesos.cli.cmds.ps.main() 60 | 61 | assert "no tasks" in self.stdout 62 | -------------------------------------------------------------------------------- /tests/integration/test_resolve.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | 22 | import zake.fake_client 23 | import zake.fake_storage 24 | 25 | import mesos.cli.cmds.resolve 26 | 27 | from .. import utils 28 | 29 | master_file = os.path.normpath(os.path.join( 30 | os.path.dirname(__file__), "..", "data", "master-host")) 31 | 32 | 33 | class TestResolve(utils.MockState): 34 | 35 | def setUp(self): # noqa 36 | super(utils.MockState, self).setUp() 37 | 38 | self.init_zk() 39 | 40 | def init_zk(self): 41 | self.storage = zake.fake_storage.FakeStorage() 42 | self.zk = zake.fake_client.FakeClient(storage=self.storage) 43 | self.zk.start() 44 | self.addCleanup(self.zk.stop) 45 | 46 | zk = zake.fake_client.FakeClient(storage=self.storage) 47 | self.mock( 48 | "mesos.cli.zookeeper.client_class", 49 | lambda *args, **kwargs: zk) 50 | 51 | self.zk.create( 52 | "/mesos/info_0000000008", 53 | utils.get_state("master.pb", parse=False), 54 | makepath=True) 55 | 56 | @utils.patch_args(["mesos-resolve", "localhost:5050"]) 57 | def test_tcp(self): 58 | mesos.cli.cmds.resolve.main() 59 | 60 | assert self.stdout == "localhost:5050\n" 61 | 62 | @utils.patch_args(["mesos-resolve", "zk://localhost:5050/mesos"]) 63 | def test_zk(self): 64 | mesos.cli.cmds.resolve.main() 65 | 66 | assert self.stdout == "10.141.141.10:5050\n" 67 | 68 | @utils.patch_args(["mesos-resolve", "file:///" + master_file]) 69 | def test_file(self): 70 | mesos.cli.cmds.resolve.main() 71 | 72 | assert self.stdout == "10.141.141.10:5050\n" 73 | -------------------------------------------------------------------------------- /tests/integration/test_scp.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import mock 21 | 22 | import mesos.cli.cmds.scp 23 | 24 | from .. import utils 25 | 26 | 27 | class TestScp(utils.MockState): 28 | 29 | @utils.patch_args([ 30 | "mesos-scp", 31 | "stdout", 32 | "/tmp" 33 | ]) 34 | def test_single(self): 35 | with mock.patch("subprocess.check_call", return_value=0) as m: 36 | mesos.cli.cmds.scp.main() 37 | 38 | m.assert_called_with( 39 | ["scp", "-pr", "stdout", "10.141.141.10:/tmp"]) 40 | assert len(self.lines) == 3 41 | assert "uploaded" in self.stdout 42 | 43 | @utils.patch_args([ 44 | "mesos-scp", 45 | "stdout", 46 | "stderr", 47 | "/tmp" 48 | ]) 49 | def test_multiple(self): 50 | with mock.patch("subprocess.check_call", return_value=0): 51 | mesos.cli.cmds.scp.main() 52 | 53 | assert len(self.lines) == 5 54 | assert "uploaded" in self.stdout 55 | -------------------------------------------------------------------------------- /tests/integration/test_ssh.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import os 21 | 22 | import mock 23 | 24 | import mesos.cli.cmds.ssh 25 | 26 | from .. import utils 27 | 28 | DIR = os.path.join( 29 | '/tmp', 'mesos', 30 | 'slaves', '20140619-151434-16842879-5050-1196-0', 31 | 'frameworks', '20140612-230025-16842879-5050-1151-0000', 32 | 'executors', 'app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2', 33 | 'runs', '3db4a3e8-52c7-4b3f-8a30-f9cb0dc3d6ba' 34 | ) 35 | 36 | 37 | class TestSsh(utils.MockState): 38 | 39 | @utils.patch_args([ 40 | "mesos-ssh", 41 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 42 | ]) 43 | def test_sandbox(self): 44 | with mock.patch("os.execvp") as m: 45 | mesos.cli.cmds.ssh.main() 46 | 47 | m.assert_called_with("ssh", [ 48 | 'ssh', 49 | '-t', 50 | '10.141.141.10', 51 | 'cd {0} && bash'.format(DIR) 52 | ]) 53 | 54 | @utils.patch_args([ 55 | "mesos-ssh", 56 | "app-215.2e6508c3-fafd-11e3-a955-b6f6cc110ef2" 57 | ]) 58 | def test_missing(self): 59 | with mock.patch("os.execvp") as m: 60 | mesos.cli.cmds.ssh.main() 61 | 62 | m.assert_called_with("ssh", [ 63 | 'ssh', 64 | '-t', 65 | '10.141.141.10' 66 | ]) 67 | 68 | @utils.patch_args([ 69 | "mesos-ssh", 70 | "a" 71 | ]) 72 | def test_partial(self): 73 | self.assertRaises(SystemExit, mesos.cli.cmds.ssh.main) 74 | 75 | assert len(self.lines) == 17 76 | -------------------------------------------------------------------------------- /tests/integration/test_state.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import json 21 | 22 | import mesos.cli.cmds.state 23 | 24 | from .. import utils 25 | 26 | 27 | class TestState(utils.MockState): 28 | 29 | @utils.patch_args(["mesos-state"]) 30 | def test_master(self): 31 | mesos.cli.cmds.state.main() 32 | assert "version" in json.loads(self.stdout) 33 | 34 | @utils.patch_args(["mesos-state", "20140619-151434-16842879-5050-1196-0"]) 35 | def test_single_slave(self): 36 | mesos.cli.cmds.state.main() 37 | 38 | val = json.loads(self.stdout) 39 | assert len(val) == 1 40 | assert val[0]["id"] == "20140619-151434-16842879-5050-1196-0" 41 | 42 | @utils.patch_args(["mesos-state", "2"]) 43 | def test_partial_match(self): 44 | mesos.cli.cmds.state.main() 45 | 46 | val = json.loads(self.stdout) 47 | assert len(val) == 2 48 | -------------------------------------------------------------------------------- /tests/integration/test_tail.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import re 21 | 22 | import mock 23 | 24 | import mesos.cli.cmds.tail 25 | 26 | from .. import utils 27 | 28 | 29 | @mock.patch("mesos.cli.mesos_file.File._fetch", utils.sandbox_read) 30 | class TestTail(utils.MockState): 31 | 32 | @utils.patch_args([ 33 | "mesos-tail", 34 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 35 | ]) 36 | def test_single_default(self): 37 | mesos.cli.cmds.tail.main() 38 | 39 | assert len(self.lines) == 6 40 | 41 | @utils.patch_args([ 42 | "mesos-tail", 43 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 44 | "stderr" 45 | ]) 46 | def test_single_specific(self): 47 | mesos.cli.cmds.tail.main() 48 | 49 | assert len(self.lines) == 9 50 | 51 | @utils.patch_args([ 52 | "mesos-tail", 53 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 54 | "st" 55 | ]) 56 | def test_partial(self): 57 | self.assertRaises(SystemExit, mesos.cli.cmds.tail.main) 58 | 59 | assert len(self.lines) == 2 60 | 61 | @utils.patch_args([ 62 | "mesos-tail", 63 | "app" 64 | ]) 65 | def test_multiple_tasks(self): 66 | mesos.cli.cmds.tail.main() 67 | 68 | assert len(self.lines) == 11 69 | 70 | @utils.patch_args([ 71 | "mesos-tail", 72 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2", 73 | "stdout", 74 | "stderr" 75 | ]) 76 | def test_multiple_files(self): 77 | mesos.cli.cmds.tail.main() 78 | 79 | assert len(re.findall("==>", self.stdout)) == 2 80 | assert len(self.lines) == 14 81 | 82 | @utils.patch_args([ 83 | "mesos-tail", 84 | "-n", "1", 85 | "app-215.3e6a099c-fcba-11e3-8b67-b6f6cc110ef2" 86 | ]) 87 | def test_line_limit(self): 88 | mesos.cli.cmds.tail.main() 89 | 90 | assert "Forked" in self.stdout 91 | assert len(self.lines) == 3 92 | 93 | @utils.patch_args([ 94 | "mesos-tail", 95 | "-q", 96 | "app" 97 | ]) 98 | def test_hide_header(self): 99 | mesos.cli.cmds.tail.main() 100 | 101 | assert len(self.lines) == 9 102 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | pass 18 | -------------------------------------------------------------------------------- /tests/unit/test_master.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import mock 20 | 21 | import mesos.cli.master 22 | 23 | from .. import utils 24 | 25 | config = { 26 | "master": "foo:5050", 27 | "scheme": "https" 28 | } 29 | 30 | 31 | class TestMaster(utils.MockState): 32 | 33 | @mock.patch.object(mesos.cli.master, 'CFG', config) 34 | def test_scheme(self): 35 | assert 'https' in mesos.cli.master.CURRENT.host 36 | -------------------------------------------------------------------------------- /tests/unit/test_slave.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import absolute_import, print_function 18 | 19 | import mock 20 | 21 | import mesos.cli.slave 22 | from mesos.cli.master import CURRENT as MASTER 23 | 24 | from .. import utils 25 | 26 | config = { 27 | "scheme": "https" 28 | } 29 | 30 | 31 | class TestMaster(utils.MockState): 32 | 33 | @mock.patch.object(mesos.cli.slave, 'CFG', config) 34 | def test_scheme(self): 35 | assert 'https' in MASTER.slaves()[0].host 36 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from __future__ import absolute_import, print_function 19 | 20 | import functools 21 | import json 22 | import os 23 | import sys 24 | 25 | import mock 26 | import testtools 27 | 28 | import mesos.cli 29 | import mesos.cli.cli 30 | import mesos.cli.exceptions 31 | 32 | 33 | def get_state(name, parse=True): 34 | path = os.path.normpath(os.path.join( 35 | os.path.dirname(__file__), "data", name)) 36 | with open(path, "rb") as fobj: 37 | val = fobj.read() 38 | if parse: 39 | return json.loads(val) 40 | else: 41 | return val 42 | 43 | 44 | def sandbox_file(path): 45 | fpath = os.path.normpath(os.path.join( 46 | os.path.dirname(__file__), "data", "sandbox", os.path.basename(path))) 47 | if not os.path.exists(fpath): 48 | raise mesos.cli.exceptions.FileDoesNotExist("") 49 | return open(fpath, "rb") 50 | 51 | 52 | # Emulate the byte fetch interface and replace with reading local files 53 | def sandbox_read(self): 54 | # This is an invalid path and the file does not exist. 55 | if self._params["path"] not in ["/master/log", "/slave/log"] and \ 56 | not self._params["path"].startswith("/tmp/mesos"): 57 | raise mesos.cli.exceptions.FileDoesNotExist("") 58 | 59 | with sandbox_file(self._params["path"]) as fobj: 60 | if self._params["offset"] == -1: 61 | fobj.seek(0, os.SEEK_END) 62 | return { 63 | "data": "", 64 | "offset": fobj.tell() 65 | } 66 | 67 | fobj.seek(self._params["offset"]) 68 | return { 69 | "data": fobj.read(self._params["length"]), 70 | "offset": self._params["offset"] 71 | } 72 | 73 | browse_state = None 74 | 75 | 76 | def file_list(self, path): 77 | if not globals()["browse_state"]: 78 | globals()["browse_state"] = get_state("browse.json") 79 | return globals()["browse_state"].get(path, []) 80 | 81 | slave_stats = mock.PropertyMock( 82 | return_value=get_state("slave_statistics.json")) 83 | 84 | patch_args = functools.partial(mock.patch, "sys.argv") 85 | 86 | 87 | class MockState(testtools.TestCase): 88 | 89 | def setUp(self): # noqa 90 | super(MockState, self).setUp() 91 | self.mock( 92 | "mesos.cli.master.MesosMaster.state", 93 | get_state("master_state.json")) 94 | self.mock( 95 | "mesos.cli.slave.MesosSlave.state", 96 | get_state("slave-20140619-151434-16842879-5050-1196-0.json")) 97 | 98 | def tearDown(self): 99 | super(MockState, self).tearDown() 100 | mesos.cli.cli.last_seen = None 101 | 102 | def mock(self, obj, val): 103 | m = mock.patch(obj, val) 104 | m.start() 105 | self.addCleanup(m.stop) 106 | 107 | @property 108 | def stdout(self): 109 | return sys.stdout.getvalue() 110 | 111 | @property 112 | def lines(self): 113 | return self.stdout.split("\n") 114 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27 3 | 4 | [testenv] 5 | commands = 6 | nosetests --where=tests [] 7 | flake8 mesos tests 8 | isort mesos/**/*.py tests/**/*.py -c -vb 9 | recreate=False 10 | deps= 11 | coverage>=3.7.1 12 | flake8>=2.2.2 13 | isort>=3.9.0 14 | mock>=1.0.1 15 | pep8-naming>=0.2.2 16 | testtools>=0.9.35 17 | zake==0.0.20 18 | nose 19 | --------------------------------------------------------------------------------