├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASES.md ├── agent ├── Cargo.toml ├── README.md └── src │ ├── errors.rs │ └── main.rs ├── bindings ├── Cargo.toml ├── README.md ├── build.rs └── src │ └── lib.rs ├── core ├── Cargo.toml ├── README.md ├── examples │ ├── basic.rs │ └── remote_host.rs └── src │ ├── command │ ├── mod.rs │ └── providers │ │ ├── generic.rs │ │ └── mod.rs │ ├── errors.rs │ ├── host │ ├── local.rs │ ├── mod.rs │ └── remote.rs │ ├── lib.rs │ ├── package │ ├── mod.rs │ └── providers │ │ ├── apt.rs │ │ ├── dnf.rs │ │ ├── homebrew.rs │ │ ├── mod.rs │ │ ├── nix.rs │ │ ├── pkg.rs │ │ └── yum.rs │ ├── remote.rs │ ├── service │ ├── mod.rs │ └── providers │ │ ├── debian.rs │ │ ├── homebrew.rs │ │ ├── launchctl.rs │ │ ├── mod.rs │ │ ├── rc.rs │ │ ├── redhat.rs │ │ └── systemd.rs │ ├── target │ ├── default.rs │ ├── linux.rs │ ├── mod.rs │ ├── redhat.rs │ └── unix.rs │ └── telemetry │ ├── mod.rs │ ├── providers │ ├── centos.rs │ ├── debian.rs │ ├── fedora.rs │ ├── freebsd.rs │ ├── macos.rs │ ├── mod.rs │ ├── nixos.rs │ └── ubuntu.rs │ └── serializable.rs └── proj ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .vagrant 5 | Vagrantfile 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | cache: cargo 10 | script: 11 | - cargo build --verbose --all 12 | - cargo test --verbose --all 13 | - cargo doc --verbose --all --no-deps 14 | deploy: 15 | provider: pages 16 | skip_cleanup: true 17 | local_dir: $TRAVIS_BUILD_DIR/target/doc 18 | github_token: $GITHUB_TOKEN 19 | on: 20 | repo: intecture/api 21 | branch: master 22 | env: 23 | - TRAVIS_CARGO_NIGHTLY_FEATURE="" 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Intecture 2 | 3 | Woo hoo! I always had a good feeling about you. Your contributions are most welcome. Thanks so much in advance for your hard work! 4 | 5 | ## Contributing 6 | 7 | You can make contributions to Intecture via GitHub pull requests. All levels of expertise are welcome. 8 | 9 | #### No, you're a git! 10 | 11 | If, upon reading this, you find yourself reconsidering that career move to landscape gardening, first take a look at the [GitHub flow](https://guides.github.com/introduction/flow/) model and GitHub's [pull request](https://help.github.com/articles/about-pull-requests/) docs. They're aimed at beginners and will hopefully demystify the contribution process. For motivation, try to picture that great day when you can order your custom-printed "Cut bugs, not grass!" t-shirt. 12 | 13 | ## Help and feedback 14 | 15 | Carl Sagan once said "There is no such thing as a dumb question". 16 | 17 | Put that to the test on our [Gitter channel](https://gitter.im/intecture/Lobby). 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "agent", 4 | "bindings", 5 | "core", 6 | "proj", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intecture APIs [![Build Status](https://travis-ci.org/intecture/api.svg?branch=master)](https://travis-ci.org/intecture/api) [![Coverage Status](https://coveralls.io/repos/github/Intecture/api/badge.svg?branch=master)](https://coveralls.io/github/Intecture/api?branch=master) [![Gitter](https://badges.gitter.im/Join\%20Chat.svg)](https://gitter.im/intecture/Lobby) 2 | 3 | **Intecture is an API for managing your servers. Visit [intecture.io](http://intecture.io).** 4 | 5 | **API docs can be found here: [intecture.io/api/intecture_api/](http://intecture.io/api/intecture_api/).** 6 | 7 | --- 8 | 9 | Intecture's APIs (_cough_ and a binary) are the heart and soul of Intecture. Check out each component's `README.md` for details: 10 | 11 | - [core](core/) - The core API that does all the heavy lifting 12 | - [bindings](bindings/) - Rust FFI and language bindings 13 | - [proj](proj/) - Helpers and boilerplate for building Intecture projects 14 | - [agent](agent/) - Tiny daemon that exposes the core API as a service (for your hosts!) 15 | 16 | ## Getting started 17 | 18 | Intecture is pretty light on external dependencies. In fact, all you'll need to [get started is Rust](https://www.rust-lang.org/install.html)! 19 | 20 | Once you've installed Rust, create a new Cargo binary project: 21 | 22 | ``` 23 | cargo new --bin 24 | ``` 25 | 26 | Next, add Intecture to your `Cargo.toml`: 27 | 28 | ``` 29 | [dependencies] 30 | futures = "0.1" 31 | intecture_api = {git = "https://github.com/intecture/api", version = "0.4"} 32 | tokio-core = "0.1" 33 | ``` 34 | 35 | This is all we need to do if we only want to manage the local machine. Just make sure you use the `Local` host type, like so: 36 | 37 | ```rust 38 | // You can ignore these two lines. They are boilerplate from `tokio-core` that 39 | // drive Intecture's asynchronous API. 40 | let mut core = Core::new().unwrap(); 41 | let handle = core.handle(); 42 | 43 | let host = Local::new(&handle).and_then(|host| { 44 | // Do stuff on the local host... 45 | }); 46 | 47 | // You can ignore this line. It is more `tokio-core` boilerplate. 48 | core.run(host).unwrap(); 49 | ``` 50 | 51 | You can find some basic examples here: [core/examples](core/examples). 52 | Also you should refer to the API docs: [intecture.io/api/intecture_api/](http://intecture.io/api/intecture_api/) 53 | 54 | #### For remote hosts only 55 | 56 | To manage a remote machine with Intecture, you need to take a few extra steps. On the remote machine... 57 | 58 | [Install Rust](https://www.rust-lang.org/install.html). 59 | 60 | Clone this GitHub repository: 61 | 62 | ``` 63 | git clone https://github.com/intecture/api 64 | ``` 65 | 66 | Build the project: 67 | 68 | ``` 69 | cd api && cargo build --release 70 | ``` 71 | 72 | Then run the agent, specifying an address to bind to: 73 | 74 | ``` 75 | target/release/intecture_agent --address 0.0.0.0:7101 76 | ``` 77 | 78 | Remember, the agent _must_ be running in order for the API to connect to this host. 79 | 80 | Finally we can get back to what we came here for - Rust codez! To manage this machine, make sure you use the `Plain` remote host type, like so: 81 | 82 | ```rust 83 | let host = Plain::connect(":7101").and_then(|host| { 84 | // Do stuff on the remote host... 85 | }); 86 | ``` 87 | 88 | Note the type is `Plain`, rather than `Remote`. At the moment, **Intecture does no encryption**, making it unsafe for use over insecure networks (i.e. the internet). The type `Plain` signifies this. In the future we will add support for encrypted remote host types as well, but for now, we cannot recommend strongly enough that you only use this on a secure local network. 89 | 90 | ## What's new? 91 | 92 | Check out [RELEASES.md](RELEASES.md) for details. 93 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # 0.4.0 (in progress) 2 | 3 | Version 0.4 is a complete rewrite of Intecture's core API. If we had been running a stable release (i.e. 1.0), this version would be version 2.0, but sadly we have to go with the far less dramatic 0.4 :( 4 | 5 | Anyway, there are some big headlines in this release: 6 | 7 | - **Hello Tokio!** This is an exciting change to our socket API, as we bid a fond farewell to ZeroMQ. The change to Tokio will bring loads of benefits, such as substantially reducing our non-Rust dependencies, and introducing strongly typed messaging between servers and clients. This will make our communications more robust and less open to exploitation. Woo hoo! 8 | - **Asynchronous, streaming endpoints with `Futures`!** This is another huge change to the API, which allows configuration tasks to be run in parallel; moreover it allows users to stream task output in realtime. Gone are the days of waiting in the dark for tasks to complete! 9 | - **Greater emphasis on composability.** Each endpoint (formerly called _primitives_) is organised into a collection of _providers_ that will (you guessed it) provide target-specific implementations of the endpoint. Users will be able to select an endpoint provider manually or let the system choose the best provider for the target platform. 10 | - **Separation of duties.** In the previous versions of Intecture, the API had been organised into a single project, making it cluttered and unwieldy. For the next release, things like the FFI, language bindings and project boilerplate will be moved into separate child projects under the same Cargo workspace. 11 | - **New error handling with `error-chain`.** In order to return less spurious errors, we've turned to `error-chain`, which provides users with context and information sorely lacking from the homemade implementation. 12 | -------------------------------------------------------------------------------- /agent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intecture_agent" 3 | version = "0.4.0" 4 | authors = [ "Pete Hayes " ] 5 | license = "MPL-2.0" 6 | description = "Tiny daemon that exposes Intecture's API as a service" 7 | homepage = "https://intecture.io" 8 | repository = "https://github.com/intecture/api" 9 | documentation = "https://intecture.io/rust/inapi/" 10 | keywords = ["intecture", "agent"] 11 | categories = ["servers"] 12 | readme = "README.md" 13 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] 14 | 15 | [badges] 16 | travis-ci = { repository = "intecture/api" } 17 | 18 | [dependencies] 19 | clap = "2.26" 20 | env_logger = "0.4" 21 | error-chain = "0.11" 22 | futures = "0.1" 23 | intecture_api = { version = "0.4.0", path = "../core" } 24 | serde = "1.0" 25 | serde_derive = "1.0" 26 | serde_json = "1.0" 27 | tokio-core = "0.1" 28 | tokio-proto = "0.1" 29 | tokio-service = "0.1" 30 | toml = "0.4" 31 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Agent 2 | 3 | Intecture Agent is a tiny daemon that exposes Intecture's core API as a service. This service should be running on each of your hosts to allow Intecture to manage them remotely. To connect to a remote host, use the `host::remote::Plain` type from [core](../core/). 4 | 5 | ## Usage 6 | 7 | To run the agent, simply execute the `intecture_agent` binary, remembering to pass it a socket address to listen on. 8 | 9 | For example, to listen to localhost on port 7101, run: 10 | 11 | ```sh 12 | intecture_agent --address localhost:7101 13 | ``` 14 | 15 | More likely though you'll want to listen on your public interface so that Intecture can talk to the host remotely. In this case you should specify the host's IP address, or use `0.0.0.0` to listen on all interfaces. 16 | 17 | ## Config file 18 | 19 | You can also store agent parameters in a configuration file. The file must be in TOML format, and can live anywhere on your server. It should look like this: 20 | 21 | ```toml 22 | address = "0.0.0.0:7101" 23 | ``` 24 | 25 | Once you've created a config file, you can start the agent by passing it the file path: 26 | 27 | ```sh 28 | intecture_agent --config agent.toml 29 | ``` 30 | -------------------------------------------------------------------------------- /agent/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use error_chain::ChainedError; 8 | use futures::Future; 9 | use intecture_api; 10 | use std::{convert, error, io}; 11 | 12 | error_chain! { 13 | links { 14 | Api(intecture_api::errors::Error, intecture_api::errors::ErrorKind); 15 | } 16 | } 17 | 18 | impl convert::From for io::Error { 19 | fn from(e: Error) -> io::Error { 20 | io::Error::new(io::ErrorKind::Other, format!("{}", e.display_chain())) 21 | } 22 | } 23 | 24 | // @todo This should disappear once Futures are officially supported 25 | // by error_chain. 26 | // See: https://github.com/rust-lang-nursery/error-chain/issues/90 27 | pub type SFuture = Box>; 28 | 29 | pub trait FutureChainErr { 30 | fn chain_err(self, callback: F) -> SFuture 31 | where F: FnOnce() -> E + 'static, 32 | E: Into; 33 | } 34 | 35 | impl FutureChainErr for F 36 | where F: Future + 'static, 37 | F::Error: error::Error + Send + 'static, 38 | { 39 | fn chain_err(self, callback: C) -> SFuture 40 | where C: FnOnce() -> E + 'static, 41 | E: Into, 42 | { 43 | Box::new(self.then(|r| r.chain_err(callback))) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /agent/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | extern crate clap; 8 | extern crate env_logger; 9 | #[macro_use] extern crate error_chain; 10 | extern crate futures; 11 | extern crate intecture_api; 12 | #[macro_use] extern crate serde_derive; 13 | extern crate serde_json; 14 | extern crate tokio_core; 15 | extern crate tokio_proto; 16 | extern crate tokio_service; 17 | extern crate toml; 18 | 19 | mod errors; 20 | 21 | use error_chain::ChainedError; 22 | use errors::*; 23 | use futures::{future, Future}; 24 | use intecture_api::host::local::Local; 25 | use intecture_api::host::remote::{JsonLineProto, LineMessage}; 26 | use intecture_api::remote::{Executable, Request, ResponseResult}; 27 | use std::fs::File; 28 | use std::io::{self, Read}; 29 | use std::net::SocketAddr; 30 | use std::sync::Arc; 31 | use tokio_core::reactor::Remote; 32 | use tokio_proto::streaming::Message; 33 | use tokio_proto::TcpServer; 34 | use tokio_service::{NewService, Service}; 35 | 36 | pub struct Api { 37 | host: Local, 38 | } 39 | 40 | pub struct NewApi { 41 | remote: Remote, 42 | } 43 | 44 | impl Service for Api { 45 | type Request = LineMessage; 46 | type Response = LineMessage; 47 | type Error = Error; 48 | type Future = Box>; 49 | 50 | fn call(&self, req: Self::Request) -> Self::Future { 51 | let req = match req { 52 | Message::WithBody(req, _) => req, 53 | Message::WithoutBody(req) => req, 54 | }; 55 | 56 | let request: Request = match serde_json::from_value(req).chain_err(|| "Could not deserialize Request") { 57 | Ok(r) => r, 58 | Err(e) => return Box::new(future::ok(error_to_msg(e))), 59 | }; 60 | 61 | Box::new(request.exec(&self.host) 62 | .chain_err(|| "Failed to execute Request") 63 | .then(|req| { 64 | match req { 65 | Ok(mut msg) => { 66 | let body = msg.take_body(); 67 | match serde_json::to_value(msg.into_inner()).chain_err(|| "Could not serialize Result") { 68 | Ok(v) => match body { 69 | Some(b) => future::ok(Message::WithBody(v, b)), 70 | None => future::ok(Message::WithoutBody(v)), 71 | }, 72 | Err(e) => future::ok(error_to_msg(e)), 73 | } 74 | }, 75 | Err(e) => future::ok(error_to_msg(e)), 76 | } 77 | })) 78 | } 79 | } 80 | 81 | impl NewService for NewApi { 82 | type Request = LineMessage; 83 | type Response = LineMessage; 84 | type Error = Error; 85 | type Instance = Api; 86 | fn new_service(&self) -> io::Result { 87 | // XXX Danger zone! If we're running multiple threads, this `unwrap()` 88 | // will explode. The API requires a `Handle`, but we can only send a 89 | // `Remote` to this Service. Currently we force the `Handle`, which is 90 | // only safe for the current thread. 91 | // See https://github.com/alexcrichton/tokio-process/issues/23 92 | let handle = self.remote.handle().unwrap(); 93 | 94 | Ok(Api { 95 | host: Local::new(&handle).wait().unwrap(), 96 | }) 97 | } 98 | } 99 | 100 | #[derive(Deserialize)] 101 | struct Config { 102 | address: SocketAddr, 103 | } 104 | 105 | quick_main!(|| -> Result<()> { 106 | env_logger::init().chain_err(|| "Could not start logging")?; 107 | 108 | let matches = clap::App::new("Intecture Agent") 109 | .version(env!("CARGO_PKG_VERSION")) 110 | .author(env!("CARGO_PKG_AUTHORS")) 111 | .about(env!("CARGO_PKG_DESCRIPTION")) 112 | .arg(clap::Arg::with_name("config") 113 | .short("c") 114 | .long("config") 115 | .value_name("FILE") 116 | .help("Path to the agent configuration file") 117 | .takes_value(true)) 118 | .arg(clap::Arg::with_name("addr") 119 | .short("a") 120 | .long("address") 121 | .value_name("ADDR") 122 | .help("Set the socket address this server will listen on (e.g. 0.0.0.0:7101)") 123 | .takes_value(true)) 124 | .group(clap::ArgGroup::with_name("config_or_else") 125 | .args(&["config", "addr"]) 126 | .required(true)) 127 | .get_matches(); 128 | 129 | let config = if let Some(c) = matches.value_of("config") { 130 | let mut fh = File::open(c).chain_err(|| "Could not open config file")?; 131 | let mut buf = Vec::new(); 132 | fh.read_to_end(&mut buf).chain_err(|| "Could not read config file")?; 133 | toml::from_slice(&buf).chain_err(|| "Config file contained invalid TOML")? 134 | } else { 135 | let address = matches.value_of("addr").unwrap().parse().chain_err(|| "Invalid server address")?; 136 | Config { address } 137 | }; 138 | 139 | // XXX We can only run a single thread here, or big boom!! 140 | // The API requires a `Handle`, but we can only send a `Remote`. 141 | // Currently we force the issue (`unwrap()`), which is only safe 142 | // for the current thread. 143 | // See https://github.com/alexcrichton/tokio-process/issues/23 144 | let server = TcpServer::new(JsonLineProto, config.address); 145 | server.with_handle(move |handle| { 146 | Arc::new(NewApi { 147 | remote: handle.remote().clone(), 148 | }) 149 | }); 150 | Ok(()) 151 | }); 152 | 153 | fn error_to_msg(e: Error) -> LineMessage { 154 | let response = ResponseResult::Err(format!("{}", e.display_chain())); 155 | // If we can't serialize this, we can't serialize anything, so 156 | // panicking is appropriate. 157 | let value = serde_json::to_value(response) 158 | .expect("Cannot serialize ResponseResult::Err. This is bad..."); 159 | Message::WithoutBody(value) 160 | } 161 | -------------------------------------------------------------------------------- /bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intecture_bindings" 3 | version = "0.4.0" 4 | authors = [ "Pete Hayes " ] 5 | license = "MPL-2.0" 6 | description = "Rust FFI and language bindings" 7 | homepage = "https://intecture.io" 8 | repository = "https://github.com/intecture/api" 9 | documentation = "https://intecture.io/rust/inapi/" 10 | keywords = ["intecture", "bindings"] 11 | categories = ["servers"] 12 | readme = "README.md" 13 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] 14 | 15 | [badges] 16 | travis-ci = { repository = "intecture/api" } 17 | 18 | [dependencies] 19 | error-chain = "0.11" 20 | intecture_api = { version = "0.4.0", path = "../core" } 21 | libc = "0.2" 22 | -------------------------------------------------------------------------------- /bindings/README.md: -------------------------------------------------------------------------------- 1 | # Bindings 2 | 3 | This project contains the Rust FFI for the project API ([proj/](../proj/)), as well as language specific bindings. 4 | 5 | ## Supported languages 6 | 7 | As Intecture's APIs are going through much upheaval, no FFI or bindings currently exist. As the APIs mature, we will port the PHP bindings from Intecture 0.3 and then look to add support for a fourth language. 8 | -------------------------------------------------------------------------------- /bindings/build.rs: -------------------------------------------------------------------------------- 1 | // dev-deps clap 2 | // Use clap to provide build options for bindings (PHP etc.) 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /bindings/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intecture_api" 3 | version = "0.4.0" 4 | authors = [ "Pete Hayes " ] 5 | license = "MPL-2.0" 6 | description = "Core API for configuring your servers" 7 | homepage = "https://intecture.io" 8 | repository = "https://github.com/intecture/api" 9 | documentation = "https://intecture.io/rust/inapi/" 10 | keywords = ["intecture", "api"] 11 | categories = ["servers"] 12 | readme = "README.md" 13 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] 14 | 15 | [badges] 16 | travis-ci = { repository = "intecture/api" } 17 | 18 | [dependencies] 19 | bytes = "0.4" 20 | erased-serde = "0.3" 21 | error-chain = "0.11" 22 | futures = "0.1" 23 | hostname = "0.1" 24 | ipnetwork = "0.12" 25 | log = "0.3" 26 | pnet = "0.20" 27 | regex = "0.2" 28 | serde = "1.0" 29 | serde_derive = "1.0" 30 | serde_json = "1.0" 31 | tokio-core = "0.1" 32 | tokio-io = "0.1" 33 | tokio-process = "0.1" 34 | tokio-proto = "0.1" 35 | tokio-service = "0.1" 36 | users = "0.6" 37 | 38 | [[example]] 39 | name = "basic" 40 | 41 | [[example]] 42 | name = "remote_host" 43 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # Core API 2 | 3 | The core API contains the endpoints used to configure a host, as well as the underlying OS abstractions that they are built on. Usually you'll use Intecture Proj ([proj/](../proj/)) instead, which reexports the core API, though for some applications, the core API will suffice. 4 | 5 | ## Project structure 6 | 7 | The core API is organised into a series of directories for each endpoint (e.g. `command/`, `host/` etc.). Within each endpoint is a `providers/` directory that houses the underlying abstractions. 8 | 9 | ## Usage 10 | 11 | For API usage, check out the [examples](examples/). 12 | -------------------------------------------------------------------------------- /core/examples/basic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | extern crate futures; 8 | extern crate intecture_api; 9 | extern crate tokio_core; 10 | 11 | use futures::{Future, Stream}; 12 | use intecture_api::prelude::*; 13 | use tokio_core::reactor::Core; 14 | 15 | fn main() { 16 | // These two lines are part of `tokio-core` and can be safely ignored. So 17 | // long as they appear at the top of your code, all is fine with the world. 18 | let mut core = Core::new().unwrap(); 19 | let handle = core.handle(); 20 | 21 | // Here's the meat of your project. In this example we're talking to our 22 | // local machine, so we use the `Local` host type. 23 | let host = Local::new(&handle).and_then(|host| { 24 | // Ok, we're in! Now we can pass our `host` handle to other endpoints, 25 | // which informs them of the server we mean to talk to. 26 | 27 | // Let's start with something basic - a shell command. 28 | let cmd = Command::new(&host, "whoami", None); 29 | cmd.exec().and_then(|mut status| { 30 | // At this point, our command is running. As the API is 31 | // asynchronous, we don't have to wait for it to finish before 32 | // inspecting its output. This is called "streaming". 33 | 34 | // First let's grab the stream from `CommandStatus`. This stream is 35 | // a stream of strings, each of which represents a line of command 36 | // output. We can use the `for_each` combinator to print these 37 | // lines to stdout. 38 | // 39 | // If printing isn't your thing, you are also free to lick them or 40 | // whatever you're into. I'm not here to judge. 41 | let stream = status.take_stream() 42 | .unwrap() // Unwrap is fine here as we haven't called it before 43 | .for_each(|line| { println!("{}", line); Ok(()) }); 44 | 45 | // Next, let's check on the result of our command. 46 | // `CommandStatus` is a `Future` that represents the command's 47 | // exit status. We can use the `map` combinator to print it out.* 48 | // 49 | // * Same caveat as above RE: printing. This is a safe 50 | // place. 51 | let status = status.map(|s| println!("This command {} {}", 52 | if s.success { "succeeded" } else { "failed" }, 53 | if let Some(e) = s.code { format!("with code {}", e) } else { String::new() })); 54 | 55 | // Finally, we need to return these two `Future`s (stream and 56 | // status) so that they will be executed by the event loop. Sadly 57 | // we can't return them both as a tuple, so we use the join 58 | // combinator instead to turn them into a single `Future`. Easy! 59 | stream.join(status) 60 | }) 61 | }); 62 | 63 | // This line is part of `tokio-core` and is used to execute the 64 | // chain of futures you've created above. You'll need to call 65 | // `core.run()` for each host you interact with, otherwise your 66 | // project will not run at all! 67 | core.run(host).unwrap(); 68 | } 69 | -------------------------------------------------------------------------------- /core/examples/remote_host.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | extern crate futures; 8 | extern crate intecture_api; 9 | extern crate tokio_core; 10 | 11 | use futures::Future; 12 | use intecture_api::prelude::*; 13 | use tokio_core::reactor::Core; 14 | 15 | fn main() { 16 | // These two lines are part of `tokio-core` and can be safely 17 | // ignored. So long as they appear at the top of your code, 18 | // all is fine with the world. 19 | let mut core = Core::new().unwrap(); 20 | let handle = core.handle(); 21 | 22 | // Here's the meat of your project. In this example we're talking 23 | // to a remote machine. You'll note that this is the `Plain` host 24 | // type, where you might have been expecting `Remote` or some such. 25 | // This is to signify that this host type sends data in the clear, 26 | // rather than encrypting it. Thus the usual disclaimer about 27 | // secure networks and trust applies. 28 | let host = Plain::connect("127.0.0.1:7101", &handle).map(|host| { 29 | // Ok, we're in! Now we can pass our `host` handle to other 30 | // endpoints, which informs them of the server we mean to 31 | // talk to. See basic.rs for more usage. 32 | println!("Connected to {}", host.telemetry().hostname); 33 | }); 34 | 35 | // This line is part of `tokio-core` and is used to execute the 36 | // chain of futures you've created above. You'll need to call 37 | // `core.run()` for each host you interact with, otherwise your 38 | // project will not run at all! 39 | core.run(host).unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /core/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! Endpoint for running shell commands. 8 | //! 9 | //! A shell command is represented by the `Command` struct, which is not 10 | //! idempotent. 11 | 12 | mod providers; 13 | 14 | use errors::*; 15 | use futures::{future, Future, Poll}; 16 | use futures::stream::Stream; 17 | use futures::sync::oneshot; 18 | use host::Host; 19 | use remote::{Request, Response}; 20 | use std::io; 21 | #[doc(hidden)] pub use self::providers::{factory, Generic}; 22 | pub use self::providers::Provider; 23 | use serde_json; 24 | use tokio_proto::streaming::{Body, Message}; 25 | 26 | #[cfg(not(windows))] 27 | const DEFAULT_SHELL: [&'static str; 2] = ["/bin/sh", "-c"]; 28 | #[cfg(windows)] 29 | const DEFAULT_SHELL: [&'static str; 1] = ["yeah...we don't currently support windows :("]; 30 | 31 | /// Represents a shell command to be executed on a host. 32 | /// 33 | ///## Examples 34 | /// 35 | /// Here's an example `ls` command that lists the directory `foo/`. 36 | /// 37 | ///``` 38 | ///extern crate futures; 39 | ///extern crate intecture_api; 40 | ///extern crate tokio_core; 41 | /// 42 | ///use futures::{Future, Stream}; 43 | ///use intecture_api::prelude::*; 44 | ///use tokio_core::reactor::Core; 45 | /// 46 | ///# fn main() { 47 | ///let mut core = Core::new().unwrap(); 48 | ///let handle = core.handle(); 49 | /// 50 | ///let host = Local::new(&handle).wait().unwrap(); 51 | /// 52 | ///let cmd = Command::new(&host, "ls /path/to/foo", None); 53 | ///let result = cmd.exec().and_then(|mut status| { 54 | /// // Print the command's stdout/stderr to stdout 55 | /// status.take_stream().unwrap() 56 | /// .for_each(|line| { println!("{}", line); Ok(()) }) 57 | /// // On its own, the stream will not do anything, so we need to make 58 | /// // sure it gets returned along with the status future. `join()` will 59 | /// // mash the two together so we can return them as one. 60 | /// .join(status.map(|s| println!("This command {} {}", 61 | /// if s.success { "succeeded" } else { "failed" }, 62 | /// if let Some(e) = s.code { 63 | /// format!("with code {}", e) 64 | /// } else { 65 | /// String::new() 66 | /// }))) 67 | ///}); 68 | /// 69 | ///core.run(result).unwrap(); 70 | ///# } 71 | ///``` 72 | /// 73 | /// We can also save all output to a string for later use. **Be careful** doing 74 | /// this as you could run out of memory on your heap if the output buffer is 75 | /// too big. 76 | /// 77 | ///```no_run 78 | ///extern crate futures; 79 | ///extern crate intecture_api; 80 | ///extern crate tokio_core; 81 | /// 82 | ///use futures::Future; 83 | ///use intecture_api::errors::*; 84 | ///use intecture_api::prelude::*; 85 | ///use tokio_core::reactor::Core; 86 | /// 87 | ///# fn main() { 88 | ///let mut core = Core::new().unwrap(); 89 | ///let handle = core.handle(); 90 | /// 91 | ///let host = Local::new(&handle).wait().unwrap(); 92 | /// 93 | ///let cmd = Command::new(&host, "ls /path/to/foo", None); 94 | ///let result = cmd.exec().and_then(|status| { 95 | /// status.result().unwrap() 96 | /// .map(|_output| { 97 | /// // Our command finished successfully. Now we can do something 98 | /// // with our output here. 99 | /// }) 100 | /// .map_err(|e| { 101 | /// // Our command errored out. Let's grab the output and see what 102 | /// // went wrong. 103 | /// match *e.kind() { 104 | /// ErrorKind::Command(ref output) => println!("Oh noes! {}", output), 105 | /// _ => unreachable!(), 106 | /// } 107 | /// e 108 | /// }) 109 | ///}); 110 | /// 111 | ///core.run(result).unwrap(); 112 | ///# } 113 | ///``` 114 | /// 115 | /// Finally, we can also ignore the stream entirely if we only care whether the 116 | /// command succeeded or not. 117 | /// 118 | ///``` 119 | ///extern crate futures; 120 | ///extern crate intecture_api; 121 | ///extern crate tokio_core; 122 | /// 123 | ///use futures::{Future, Stream}; 124 | ///use intecture_api::prelude::*; 125 | ///use tokio_core::reactor::Core; 126 | /// 127 | ///# fn main() { 128 | ///let mut core = Core::new().unwrap(); 129 | ///let handle = core.handle(); 130 | /// 131 | ///let host = Local::new(&handle).wait().unwrap(); 132 | /// 133 | ///let cmd = Command::new(&host, "ls /path/to/foo", None); 134 | ///let result = cmd.exec().and_then(|mut status| { 135 | /// status.map(|exit_status| { 136 | /// if exit_status.success { 137 | /// println!("Huzzah!"); 138 | /// } else { 139 | /// println!("Doh!"); 140 | /// } 141 | /// }) 142 | ///}); 143 | /// 144 | ///core.run(result).unwrap(); 145 | ///# } 146 | ///``` 147 | pub struct Command { 148 | host: H, 149 | provider: Option, 150 | cmd: Vec, 151 | } 152 | 153 | /// Represents the status of a running `Command`, including the output stream 154 | /// and exit status. 155 | pub struct CommandStatus { 156 | stream: Option>>, 157 | exit_status: Option>>, 158 | } 159 | 160 | /// Represents the exit status of a `Command` as a `Result`-like `Future`. If 161 | /// the command succeeded, the command output is returned. If it failed, an 162 | /// error containing the command's output is returned. 163 | pub struct CommandResult { 164 | inner: Box>, 165 | } 166 | 167 | /// The status of a finished command. 168 | /// 169 | /// This is a serializable replica of 170 | /// [`std::process::ExitStatus`](https://doc.rust-lang.org/std/process/struct.ExitStatus.html). 171 | #[derive(Debug, Serialize, Deserialize)] 172 | pub struct ExitStatus { 173 | /// Was termination successful? Signal termination is not considered a 174 | /// success, and success is defined as a zero exit status. 175 | pub success: bool, 176 | /// Returns the exit code of the process, if any. 177 | /// 178 | /// On Unix, this will return `None` if the process was terminated by a 179 | /// signal. 180 | pub code: Option, 181 | } 182 | 183 | impl Command { 184 | /// Create a new `Command` with the default [`Provider`](enum.Provider.html). 185 | /// 186 | /// By default, `Command` will use `/bin/sh -c` as the shell. You can 187 | /// override this by providing a value for `shell`. Note that the 188 | /// underlying implementation of `Command` escapes whitespace, so each 189 | /// argument needs to be a separate item in the slice. For example, to use 190 | /// Bash as your shell, you'd provide the value: 191 | /// `Some(&["/bin/bash", "-c"])`. 192 | pub fn new(host: &H, cmd: &str, shell: Option<&[&str]>) -> Command { 193 | let mut args: Vec = shell.unwrap_or(&DEFAULT_SHELL).to_owned() 194 | .iter().map(|a| (*a).to_owned()).collect(); 195 | args.push(cmd.into()); 196 | 197 | Command { 198 | host: host.clone(), 199 | provider: None, 200 | cmd: args, 201 | } 202 | } 203 | 204 | /// Create a new `Command` with the specified [`Provider`](enum.Provider.html). 205 | /// 206 | ///## Example 207 | ///``` 208 | ///extern crate futures; 209 | ///extern crate intecture_api; 210 | ///extern crate tokio_core; 211 | /// 212 | ///use futures::Future; 213 | ///use intecture_api::command::Provider; 214 | ///use intecture_api::prelude::*; 215 | ///use tokio_core::reactor::Core; 216 | /// 217 | ///# fn main() { 218 | ///let mut core = Core::new().unwrap(); 219 | ///let handle = core.handle(); 220 | /// 221 | ///let host = Local::new(&handle).wait().unwrap(); 222 | /// 223 | ///Command::with_provider(&host, Provider::Generic, "ls /path/to/foo", None); 224 | ///# } 225 | pub fn with_provider(host: &H, provider: Provider, cmd: &str, shell: Option<&[&str]>) -> Command { 226 | let mut cmd = Self::new(host, cmd, shell); 227 | cmd.provider = Some(provider); 228 | cmd 229 | } 230 | 231 | /// Execute the command. 232 | /// 233 | ///## Returns 234 | /// 235 | /// This function returns a `Future` that represents the delay between 236 | /// now and the time it takes to start execution. This `Future` yields a 237 | /// tuple with a `Stream` and a `Future` inside. The `Stream` is the 238 | /// command's output stream, including both stdout and stderr. The `Future` 239 | /// yields the command's `ExitStatus`. 240 | /// 241 | /// **WARNING!** For remote `Host` types, you _MUST_ consume the output 242 | /// `Stream` if you want to access the `ExitStatus`. This is due to the 243 | /// plumbing between the API and the remote host, which relies on a single 244 | /// streaming pipe. First we stream the command output, then tack the 245 | /// `ExitStatus` on as the last frame. Without consuming the output buffer, 246 | /// we would never be able to get to the last frame, and `ExitStatus` could 247 | /// never be resolved. 248 | /// 249 | ///# Errors 250 | /// 251 | ///>Error: Buffer dropped before ExitStatus was sent 252 | /// 253 | ///>Caused by: oneshot canceled 254 | /// 255 | /// This is the error you'll see if you prematurely drop the output `Stream` 256 | /// while trying to resolve the `Future`. 257 | pub fn exec(&self) -> Box> { 258 | let request = Request::CommandExec(self.provider, self.cmd.clone()); 259 | Box::new(self.host.request(request) 260 | .chain_err(|| ErrorKind::Request { endpoint: "Command", func: "exec" }) 261 | .map(|msg| { 262 | CommandStatus::new(msg) 263 | })) 264 | } 265 | } 266 | 267 | impl CommandStatus { 268 | #[doc(hidden)] 269 | pub fn new(mut msg: Message, io::Error>>) -> CommandStatus { 270 | let (tx, rx) = oneshot::channel::(); 271 | let mut tx = Some(tx); 272 | let stream = msg.take_body() 273 | .expect("Command::exec reply missing body stream") 274 | .filter_map(move |v| { 275 | let s = String::from_utf8_lossy(&v).to_string(); 276 | 277 | // @todo This is a heuristical approach which is fallible 278 | if s.starts_with("ExitStatus:") { 279 | let (_, json) = s.split_at(11); 280 | match serde_json::from_str(json) { 281 | Ok(status) => { 282 | // @todo What should happen if this fails? 283 | let _ = tx.take().unwrap().send(status); 284 | return None; 285 | }, 286 | _ => (), 287 | } 288 | } 289 | 290 | Some(s) 291 | }) 292 | .then(|r| r.chain_err(|| "Command execution failed")); 293 | 294 | let exit_status = rx.chain_err(|| "Buffer dropped before ExitStatus was sent"); 295 | 296 | CommandStatus { 297 | stream: Some(Box::new(stream)), 298 | exit_status: Some(Box::new(exit_status)), 299 | } 300 | } 301 | 302 | /// Take ownership of the output stream. 303 | /// 304 | /// The stream is guaranteed to be present only if this is the first call 305 | /// to `take_stream()` and the future has not yet been polled. 306 | pub fn take_stream(&mut self) -> Option>> { 307 | self.stream.take() 308 | } 309 | 310 | /// Convert this to a `CommandResult`, which returns the output string on 311 | /// success and an error containing the command's output on failure. If the 312 | /// stream has already been taken by `take_stream()` then this function 313 | /// will return `None`. 314 | /// 315 | /// Note that "success" is determined by examining the `ExitStatus::success` 316 | /// bool. See `ExitStatus` docs for details. 317 | pub fn result(self) -> Option { 318 | if let Some(stream) = self.stream { 319 | let inner = stream.fold(String::new(), |mut acc, line| { 320 | acc.push_str(&line); 321 | future::ok::<_, Error>(acc) 322 | }) 323 | .join(self.exit_status.unwrap()) 324 | .and_then(|(output, status)| if status.success { 325 | future::ok(output) 326 | } else { 327 | future::err(ErrorKind::Command(output).into()) 328 | }); 329 | 330 | Some(CommandResult { 331 | inner: Box::new(inner) as Box> 332 | }) 333 | } else { 334 | None 335 | } 336 | } 337 | } 338 | 339 | impl Future for CommandStatus { 340 | type Item = ExitStatus; 341 | type Error = Error; 342 | 343 | fn poll(&mut self) -> Poll { 344 | if let Some(stream) = self.stream.take() { 345 | self.exit_status = Some(Box::new(stream.for_each(|_| Ok(())) 346 | .join(self.exit_status.take().unwrap()) 347 | .map(|(_, status)| status))); 348 | } 349 | 350 | self.exit_status.as_mut().unwrap().poll() 351 | } 352 | } 353 | 354 | impl Future for CommandResult { 355 | type Item = String; 356 | type Error = Error; 357 | 358 | fn poll(&mut self) -> Poll { 359 | self.inner.poll() 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /core/src/command/providers/generic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::{future, Future}; 9 | use futures::sink::Sink; 10 | use futures::stream::Stream; 11 | use futures::sync::mpsc; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use serde_json; 14 | use std::io::{self, BufReader}; 15 | use std::process::{Command, Stdio}; 16 | use std::result; 17 | use super::{CommandProvider, ExitStatus}; 18 | use tokio_core::reactor::Handle; 19 | use tokio_io::io::lines; 20 | use tokio_process::CommandExt; 21 | use tokio_proto::streaming::{Body, Message}; 22 | 23 | pub struct Generic; 24 | 25 | impl CommandProvider for Generic { 26 | fn available() -> bool { 27 | true 28 | } 29 | 30 | fn exec(&self, handle: &Handle, cmd: &[&str]) -> ExecutableResult { 31 | let (cmd, cmd_args) = match cmd.split_first() { 32 | Some((s, a)) => (s, a), 33 | None => return Box::new(future::err("Invalid shell provided".into())), 34 | }; 35 | 36 | let child = Command::new(cmd) 37 | .args(cmd_args) 38 | .stdout(Stdio::piped()) 39 | .stderr(Stdio::piped()) 40 | .spawn_async(handle) 41 | .chain_err(|| "Command execution failed"); 42 | let mut child = match child { 43 | Ok(c) => c, 44 | Err(e) => return Box::new(future::err(e)), 45 | }; 46 | 47 | let (tx1, body) = Body::pair(); 48 | let tx2 = tx1.clone(); 49 | 50 | let stdout = child.stdout().take().unwrap(); 51 | let outbuf = BufReader::new(stdout); 52 | let stderr = child.stderr().take().unwrap(); 53 | let errbuf = BufReader::new(stderr); 54 | 55 | let status = child.map_err(|e| Error::with_chain(e, ErrorKind::Msg("Command execution failed".into()))) 56 | .and_then(|s| { 57 | let status = ExitStatus { 58 | success: s.success(), 59 | code: s.code(), 60 | }; 61 | match serde_json::to_string(&status) 62 | .chain_err(|| "Could not serialize `ExitStatus` struct") 63 | { 64 | Ok(s) => { 65 | let mut frame = "ExitStatus:".to_owned(); 66 | frame.push_str(&s); 67 | Box::new(tx2.send(Ok(frame.into_bytes())) 68 | .map_err(|e| Error::with_chain(e, "Could not forward command output to Body")) 69 | ) as Box, io::Error>>, Error = Error>> 70 | }, 71 | Err(e) => Box::new(future::err(e)), 72 | } 73 | }); 74 | 75 | let stream = lines(outbuf) 76 | .select(lines(errbuf)) 77 | .map(|s| Ok(s.into_bytes())) 78 | .map_err(|e| Error::with_chain(e, ErrorKind::Msg("Command execution failed".into()))) 79 | .forward(tx1.sink_map_err(|e| Error::with_chain(e, "Could not forward command output to Body"))) 80 | .join(status) 81 | // @todo We should repatriate these errors somehow 82 | .map(|_| ()) 83 | .map_err(|_| ()); 84 | 85 | handle.spawn(stream); 86 | 87 | Box::new(future::ok(Message::WithBody(ResponseResult::Ok(Response::Null), body))) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /core/src/command/providers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! OS abstractions for `Command`. 8 | 9 | mod generic; 10 | 11 | use command::ExitStatus; 12 | use errors::*; 13 | use remote::ExecutableResult; 14 | pub use self::generic::Generic; 15 | use tokio_core::reactor::Handle; 16 | 17 | /// Specific implementation of `Command` 18 | #[derive(Clone, Copy, Serialize, Deserialize)] 19 | pub enum Provider { 20 | Generic, 21 | } 22 | 23 | pub trait CommandProvider { 24 | fn available() -> bool where Self: Sized; 25 | fn exec(&self, &Handle, &[&str]) -> ExecutableResult; 26 | } 27 | 28 | #[doc(hidden)] 29 | pub fn factory() -> Result> { 30 | if Generic::available() { 31 | Ok(Box::new(Generic)) 32 | } else { 33 | Err(ErrorKind::ProviderUnavailable("Command").into()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! The API error type. 8 | 9 | use futures::Future; 10 | use regex; 11 | use std::{error, io}; 12 | 13 | error_chain! { 14 | foreign_links { 15 | Io(io::Error); 16 | Regex(regex::Error); 17 | } 18 | 19 | errors { 20 | Command(out: String) { 21 | description("Command returned non-zero exit code"), 22 | display("Command returned non-zero exit code with output: {}", out), 23 | } 24 | 25 | InvalidTelemetryKey { 26 | cmd: &'static str, 27 | key: String, 28 | } { 29 | description("Provided key not found in output"), 30 | display("Provided key '{}' not found in {} output", key, cmd), 31 | } 32 | 33 | ProviderUnavailable(p: &'static str) { 34 | description("No providers available"), 35 | display("No providers available for {}", p), 36 | } 37 | 38 | Request { 39 | endpoint: &'static str, 40 | func: &'static str, 41 | } { 42 | description("Could not run provider function on host"), 43 | display("Could not run {}::{}() on host", endpoint, func), 44 | } 45 | 46 | Remote(e: String) { 47 | description("Error running command on remote host"), 48 | display("Error running command on remote host: {}", e), 49 | } 50 | 51 | SystemCommand(c: &'static str) { 52 | description("Error running system command"), 53 | display("Error running system command '{}'", c), 54 | } 55 | 56 | SystemCommandOutput(c: &'static str) { 57 | description("Could not understand output of system command"), 58 | display("Could not understand output of system command '{}'", c), 59 | } 60 | 61 | SystemFile(c: &'static str) { 62 | description("Could not open system file"), 63 | display("Could not open system file '{}'", c), 64 | } 65 | 66 | SystemFileOutput(c: &'static str) { 67 | description("Could not understand output of system file"), 68 | display("Could not understand output of system file '{}'", c), 69 | } 70 | } 71 | } 72 | 73 | // @todo This should disappear once Futures are officially supported 74 | // by error_chain. 75 | // See: https://github.com/rust-lang-nursery/error-chain/issues/90 76 | pub type SFuture = Box>; 77 | 78 | pub trait FutureChainErr { 79 | fn chain_err(self, callback: F) -> SFuture 80 | where F: FnOnce() -> E + 'static, 81 | E: Into; 82 | } 83 | 84 | impl FutureChainErr for F 85 | where F: Future + 'static, 86 | F::Error: error::Error + Send + 'static, 87 | { 88 | fn chain_err(self, callback: C) -> SFuture 89 | where C: FnOnce() -> E + 'static, 90 | E: Into, 91 | { 92 | Box::new(self.then(|r| r.chain_err(callback))) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/host/local.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! A connection to the local machine. 8 | 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use remote::{Executable, Request, Response, ResponseResult}; 12 | use std::io; 13 | use std::sync::Arc; 14 | use super::Host; 15 | use telemetry::{self, Telemetry}; 16 | // use telemetry::Telemetry; 17 | use tokio_core::reactor::Handle; 18 | use tokio_proto::streaming::{Body, Message}; 19 | 20 | /// A `Host` type that talks directly to the local machine. 21 | #[derive(Clone)] 22 | pub struct Local { 23 | inner: Arc, 24 | handle: Handle, 25 | } 26 | 27 | struct Inner { 28 | telemetry: Option, 29 | } 30 | 31 | impl Local { 32 | /// Create a new `Host` targeting the local machine. 33 | pub fn new(handle: &Handle) -> Box> { 34 | let mut host = Local { 35 | inner: Arc::new(Inner { telemetry: None }), 36 | handle: handle.clone(), 37 | }; 38 | 39 | Box::new(telemetry::Telemetry::load(&host) 40 | .chain_err(|| "Could not load telemetry for host") 41 | .map(|t| { 42 | Arc::get_mut(&mut host.inner).unwrap().telemetry = Some(t); 43 | host 44 | })) 45 | } 46 | } 47 | 48 | impl Host for Local { 49 | fn telemetry(&self) -> &Telemetry { 50 | self.inner.telemetry.as_ref().unwrap() 51 | } 52 | 53 | fn handle(&self) -> &Handle { 54 | &self.handle 55 | } 56 | 57 | #[doc(hidden)] 58 | fn request_msg(&self, msg: Message, io::Error>>) -> 59 | Box, io::Error>>, Error = Error>> 60 | { 61 | Box::new(msg.into_inner() 62 | .exec(self) 63 | .and_then(|mut msg| { 64 | let body = msg.take_body(); 65 | match msg.into_inner() { 66 | ResponseResult::Ok(response) => if let Some(body) = body { 67 | future::ok(Message::WithBody(response, body)) 68 | } else { 69 | future::ok(Message::WithoutBody(response)) 70 | }, 71 | ResponseResult::Err(e) => future::err(e.into()), 72 | } 73 | })) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/src/host/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! Manages the connection between the API and a server. 8 | 9 | pub mod local; 10 | pub mod remote; 11 | 12 | use errors::*; 13 | use futures::Future; 14 | use remote::{Request, Response}; 15 | use std::io; 16 | use telemetry::Telemetry; 17 | use tokio_core::reactor::Handle; 18 | use tokio_proto::streaming::{Body, Message}; 19 | 20 | /// Trait for local and remote host types. 21 | pub trait Host: Clone { 22 | /// Get `Telemetry` for this host. 23 | fn telemetry(&self) -> &Telemetry; 24 | /// Get `Handle` to Tokio reactor. 25 | fn handle(&self) -> &Handle; 26 | #[doc(hidden)] 27 | fn request(&self, request: Request) -> 28 | Box, io::Error>>, Error = Error>> 29 | { 30 | self.request_msg(Message::WithoutBody(request)) 31 | } 32 | #[doc(hidden)] 33 | fn request_msg(&self, Message, io::Error>>) -> 34 | Box, io::Error>>, Error = Error>>; 35 | } 36 | -------------------------------------------------------------------------------- /core/src/host/remote.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! A connection to a remote host. 8 | 9 | use bytes::BytesMut; 10 | use errors::*; 11 | use futures::{future, Future}; 12 | use remote::{Request, Response, ResponseResult}; 13 | use serde_json; 14 | use std::{io, result}; 15 | use std::net::SocketAddr; 16 | use std::sync::Arc; 17 | use super::Host; 18 | use telemetry::{self, Telemetry}; 19 | use tokio_core::reactor::Handle; 20 | use tokio_io::{AsyncRead, AsyncWrite}; 21 | use tokio_io::codec::{Encoder, Decoder, Framed}; 22 | use tokio_proto::streaming::{Body, Message}; 23 | use tokio_proto::streaming::pipeline::{ClientProto, Frame, ServerProto}; 24 | use tokio_proto::TcpClient; 25 | use tokio_proto::util::client_proxy::ClientProxy; 26 | use tokio_service::Service; 27 | 28 | #[doc(hidden)] 29 | pub type LineMessage = Message, io::Error>>; 30 | 31 | /// A `Host` type that uses an unencrypted socket. 32 | /// 33 | /// >**Warning!** An unencrypted host is susceptible to eavesdropping and MITM 34 | /// attacks, and should only be used on secure private networks. 35 | #[derive(Clone)] 36 | pub struct Plain { 37 | inner: Arc, 38 | handle: Handle, 39 | } 40 | 41 | struct Inner { 42 | inner: ClientProxy, 43 | telemetry: Option, 44 | } 45 | 46 | #[doc(hidden)] 47 | pub struct JsonLineCodec { 48 | decoding_head: bool, 49 | } 50 | #[doc(hidden)] 51 | pub struct JsonLineProto; 52 | 53 | impl Plain { 54 | /// Create a new Host connected to the given address. 55 | pub fn connect(addr: &str, handle: &Handle) -> Box> { 56 | let addr: SocketAddr = match addr.parse().chain_err(|| "Invalid host address") { 57 | Ok(addr) => addr, 58 | Err(e) => return Box::new(future::err(e)), 59 | }; 60 | let handle = handle.clone(); 61 | 62 | info!("Connecting to host {}", addr); 63 | 64 | Box::new(TcpClient::new(JsonLineProto) 65 | .connect(&addr, &handle) 66 | .chain_err(|| "Could not connect to host") 67 | .and_then(move |client_service| { 68 | info!("Connected!"); 69 | 70 | let mut host = Plain { 71 | inner: Arc::new( 72 | Inner { 73 | inner: client_service, 74 | telemetry: None, 75 | }), 76 | handle: handle.clone(), 77 | }; 78 | 79 | telemetry::Telemetry::load(&host) 80 | .chain_err(|| "Could not load telemetry for host") 81 | .map(|t| { 82 | Arc::get_mut(&mut host.inner).unwrap().telemetry = Some(t); 83 | host 84 | }) 85 | })) 86 | } 87 | } 88 | 89 | impl Host for Plain { 90 | fn telemetry(&self) -> &Telemetry { 91 | self.inner.telemetry.as_ref().unwrap() 92 | } 93 | 94 | fn handle(&self) -> &Handle { 95 | &self.handle 96 | } 97 | 98 | #[doc(hidden)] 99 | fn request_msg(&self, msg: Message, io::Error>>) -> 100 | Box, io::Error>>, Error = Error>> 101 | { 102 | self.call(msg) 103 | } 104 | } 105 | 106 | impl Service for Plain { 107 | type Request = Message, io::Error>>; 108 | type Response = Message, io::Error>>; 109 | type Error = Error; 110 | type Future = Box>; 111 | 112 | fn call(&self, mut req: Self::Request) -> Self::Future { 113 | let body = req.take_body(); 114 | let request = req.into_inner(); 115 | 116 | let value = match serde_json::to_value(request).chain_err(|| "Could not encode provider to send to host") { 117 | Ok(v) => v, 118 | Err(e) => return Box::new(future::err(e)) 119 | }; 120 | 121 | debug!("Sending JSON request: {}", value); 122 | 123 | let json_msg = match body { 124 | Some(b) => Message::WithBody(value, b), 125 | None => Message::WithoutBody(value), 126 | }; 127 | 128 | Box::new(self.inner.inner.call(json_msg) 129 | .chain_err(|| "Error while running provider on host") 130 | .and_then(|mut msg| { 131 | let body = msg.take_body(); 132 | let header = msg.into_inner(); 133 | 134 | debug!("Received JSON response: {}", header); 135 | 136 | let result: ResponseResult = match serde_json::from_value(header).chain_err(|| "Could not understand response from host") { 137 | Ok(d) => d, 138 | Err(e) => return Box::new(future::err(e)), 139 | }; 140 | 141 | let msg = match result { 142 | ResponseResult::Ok(msg) => msg, 143 | ResponseResult::Err(e) => return Box::new(future::err(ErrorKind::Remote(e).into())), 144 | }; 145 | Box::new(future::ok(match body { 146 | Some(b) => Message::WithBody(msg, b), 147 | None => Message::WithoutBody(msg), 148 | })) 149 | })) 150 | } 151 | } 152 | 153 | impl Decoder for JsonLineCodec { 154 | type Item = Frame, io::Error>; 155 | type Error = io::Error; 156 | 157 | fn decode(&mut self, buf: &mut BytesMut) -> io::Result> { 158 | let line = match buf.iter().position(|b| *b == b'\n') { 159 | Some(n) => buf.split_to(n), 160 | None => return Ok(None), 161 | }; 162 | 163 | buf.split_to(1); 164 | 165 | if self.decoding_head { 166 | debug!("Decoding header: {:?}", line); 167 | 168 | // The last byte in this frame is a bool that indicates 169 | // whether we have a body stream following or not. 170 | // This byte must exist, or our codec is buggered and 171 | // panicking is appropriate. 172 | let (has_body, line) = line.split_last() 173 | .expect("Missing body byte at end of message frame"); 174 | 175 | debug!("Body byte: {:?}", has_body); 176 | 177 | if *has_body == 1 { 178 | self.decoding_head = false; 179 | } 180 | 181 | let frame = Frame::Message { 182 | message: serde_json::from_slice(&line).map_err(|e| { 183 | io::Error::new(io::ErrorKind::Other, e) 184 | })?, 185 | body: *has_body == 1, 186 | }; 187 | 188 | debug!("Decoded header: {:?}", frame); 189 | 190 | Ok(Some(frame)) 191 | } else { 192 | debug!("Decoding body chunk: {:?}", line); 193 | 194 | let frame = if line.is_empty() { 195 | self.decoding_head = true; 196 | Frame::Body { chunk: None } 197 | } else { 198 | Frame::Body { chunk: Some(line.to_vec()) } 199 | }; 200 | 201 | debug!("Decoded body chunk: {:?}", frame); 202 | 203 | Ok(Some(frame)) 204 | } 205 | } 206 | } 207 | 208 | impl Encoder for JsonLineCodec { 209 | type Item = Frame, io::Error>; 210 | type Error = io::Error; 211 | 212 | fn encode(&mut self, msg: Self::Item, buf: &mut BytesMut) -> io::Result<()> { 213 | match msg { 214 | Frame::Message { message, body } => { 215 | debug!("Encoding header: {:?}, {:?}", message, body); 216 | 217 | let json = serde_json::to_vec(&message) 218 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 219 | buf.extend(&json); 220 | // Add 'has-body' flag 221 | buf.extend(if body { &[1] } else { &[0] }); 222 | } 223 | Frame::Body { chunk } => { 224 | debug!("Encoding chunk: {:?}", chunk); 225 | 226 | if let Some(chunk) = chunk { 227 | buf.extend(&chunk); 228 | } 229 | } 230 | Frame::Error { error } => { 231 | // @todo Support error frames 232 | return Err(error) 233 | } 234 | } 235 | 236 | buf.extend(b"\n"); 237 | 238 | Ok(()) 239 | } 240 | } 241 | 242 | impl ClientProto for JsonLineProto { 243 | type Request = serde_json::Value; 244 | type RequestBody = Vec; 245 | type Response = serde_json::Value; 246 | type ResponseBody = Vec; 247 | type Error = io::Error; 248 | type Transport = Framed; 249 | type BindTransport = result::Result; 250 | 251 | fn bind_transport(&self, io: T) -> Self::BindTransport { 252 | let codec = JsonLineCodec { 253 | decoding_head: true, 254 | }; 255 | 256 | Ok(io.framed(codec)) 257 | } 258 | } 259 | 260 | impl ServerProto for JsonLineProto { 261 | type Request = serde_json::Value; 262 | type RequestBody = Vec; 263 | type Response = serde_json::Value; 264 | type ResponseBody = Vec; 265 | type Error = io::Error; 266 | type Transport = Framed; 267 | type BindTransport = result::Result; 268 | 269 | fn bind_transport(&self, io: T) -> Self::BindTransport { 270 | let codec = JsonLineCodec { 271 | decoding_head: true, 272 | }; 273 | 274 | Ok(io.framed(codec)) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! Intecture is an API for managing your servers. You can think of it as a 8 | //! DevOps tool, but without the complicated ecosystem and proprietary nonsense. 9 | //! 10 | //! The core API is, well, the core of Intecture. It contains all the endpoints 11 | //! used to configure a host, as well as the underlying OS abstractions that 12 | //! they are built on. Generally you'll consume this API via 13 | //! [intecture_proj](../intecture_proj/), which reexports `intecture_api`, 14 | //! though for projects that do not need a formal structure (e.g. an installer 15 | //! program), this API will suffice. 16 | //! 17 | //!## Project structure 18 | //! 19 | //! The core API is organised into a series of modules (known as “endpoints”, 20 | //! e.g. `command`, `package` etc.), which represent basic configuration tasks 21 | //! that you’d normally perform by hand. Within each endpoint is a `providers` 22 | //! module, which houses the OS-specific abstractions that do the heavy lifting 23 | //! on behalf of the endpoint. 24 | //! 25 | //! For example, the [`package`](package/) endpoint has a struct called 26 | //! `Package`. This is a cross-platform abstraction for managing a package on 27 | //! your server. Behind this abstraction is a concrete implementation of a 28 | //! specific package [_provider_](package/providers), e.g. Yum or Apt. If you 29 | //! instantiate a new `Package` instance through the 30 | //! [`Package::new()`](package/struct.Package.html#method.new) function, the 31 | //! best available provider for your server is chosen automatically. This is 32 | //! true of all endpoints. 33 | //! 34 | //!## Hosts 35 | //! 36 | //! So far we’ve talked about using endpoints to automate configuration tasks, 37 | //! but how does Intecture know which server we want to talk to? This is where 38 | //! we need the [`host`](host/) endpoint. All things start with a host! Side 39 | //! note - if we were ever to do ‘merch’, that’d probably be on a t-shirt. 40 | //! Anyway, poor marketing decisions aside, you’ll need to create a host in 41 | //! order to do anything. 42 | //! 43 | //! Hosts come in both the [`Local`](host/local/struct.Local.html) and 44 | //! [`Plain`](host/remote/struct.Plain.html) varieties. The `Local` type points 45 | //! to your local machine, and the `Plain` type is a remote host type that 46 | //! connects to a remote machine over the network. Whichever type you choose, 47 | //! simply pass it in to your endpoints as required and Intecture will do the 48 | //! rest. 49 | //! 50 | //!>“Why `Plain`?” I hear you ask. Well, it’s because the `Plain` host type is 51 | //! a remote host that uses TCP to send/receive _plaintext_ data. 52 | //! 53 | //!## Example 54 | //! 55 | //! Here’s a reproduction of the 56 | //! [basic example](https://github.com/intecture/api/blob/master/core/examples/basic.rs) 57 | //! from the `examples/` folder: 58 | //! 59 | //!```rust 60 | //!extern crate futures; 61 | //!extern crate intecture_api; 62 | //!extern crate tokio_core; 63 | //! 64 | //!use futures::{Future, Stream}; 65 | //!use intecture_api::prelude::*; 66 | //!use tokio_core::reactor::Core; 67 | //! 68 | //!fn main() { 69 | //! // These two lines are part of `tokio-core` and can be safely ignored. So 70 | //! // long as they appear at the top of your code, all is fine with the world. 71 | //! let mut core = Core::new().unwrap(); 72 | //! let handle = core.handle(); 73 | //! 74 | //! // Here's the meat of your project. In this example we're talking to our 75 | //! // local machine, so we use the `Local` host type. 76 | //! let host = Local::new(&handle).and_then(|host| { 77 | //! // Ok, we're in! Now we can pass our `host` handle to other endpoints, 78 | //! // which informs them of the server we mean to talk to. 79 | //! 80 | //! // Let's start with something basic - a shell command. 81 | //! let cmd = Command::new(&host, "whoami", None); 82 | //! cmd.exec().and_then(|mut status| { 83 | //! // At this point, our command is running. As the API is 84 | //! // asynchronous, we don't have to wait for it to finish before 85 | //! // inspecting its output. This is called "streaming". 86 | //! 87 | //! // First let's grab the stream from `CommandStatus`. This stream is 88 | //! // a stream of strings, each of which represents a line of command 89 | //! // output. We can use the `for_each` combinator to print these 90 | //! // lines to stdout. 91 | //! // 92 | //! // If printing isn't your thing, you are also free to lick them or 93 | //! // whatever you're into. I'm not here to judge. 94 | //! let stream = status.take_stream() 95 | //! .unwrap() // Unwrap is fine here as we haven't called it before 96 | //! .for_each(|line| { println!("{}", line); Ok(()) }); 97 | //! 98 | //! // Next, let's check on the result of our command. 99 | //! // `CommandStatus` is a `Future` that represents the command's 100 | //! // exit status. We can use the `map` combinator to print it out.* 101 | //! // 102 | //! // * Same caveat as above RE: printing. This is a safe 103 | //! // place. 104 | //! let status = status.map(|s| println!("This command {} {}", 105 | //! if s.success { "succeeded" } else { "failed" }, 106 | //! if let Some(e) = s.code { format!("with code {}", e) } else { String::new() })); 107 | //! 108 | //! // Finally, we need to return these two `Future`s (stream and 109 | //! // status) so that they will be executed by the event loop. Sadly 110 | //! // we can't return them both as a tuple, so we use the join 111 | //! // combinator instead to turn them into a single `Future`. Easy! 112 | //! stream.join(status) 113 | //! }) 114 | //! }); 115 | //! 116 | //! // This line is part of `tokio-core` and is used to execute the 117 | //! // chain of futures you've created above. You'll need to call 118 | //! // `core.run()` for each host you interact with, otherwise your 119 | //! // project will not run at all! 120 | //! core.run(host).unwrap(); 121 | //!} 122 | //!``` 123 | 124 | #![recursion_limit = "1024"] 125 | 126 | extern crate bytes; 127 | extern crate erased_serde; 128 | #[macro_use] extern crate error_chain; 129 | extern crate futures; 130 | extern crate hostname; 131 | extern crate ipnetwork; 132 | #[macro_use] extern crate log; 133 | extern crate pnet; 134 | extern crate regex; 135 | extern crate serde; 136 | #[macro_use] extern crate serde_derive; 137 | extern crate serde_json; 138 | extern crate tokio_core; 139 | extern crate tokio_io; 140 | extern crate tokio_process; 141 | extern crate tokio_proto; 142 | extern crate tokio_service; 143 | extern crate users; 144 | 145 | pub mod command; 146 | pub mod errors; 147 | pub mod host; 148 | pub mod prelude { 149 | //! The API prelude. 150 | pub use command::{self, Command}; 151 | pub use host::Host; 152 | pub use host::remote::{self, Plain}; 153 | pub use host::local::{self, Local}; 154 | pub use package::{self, Package}; 155 | pub use service::{self, Service}; 156 | pub use telemetry::{self, Cpu, FsMount, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 157 | } 158 | pub mod package; 159 | #[doc(hidden)] pub mod remote; 160 | pub mod service; 161 | mod target; 162 | pub mod telemetry; 163 | -------------------------------------------------------------------------------- /core/src/package/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! Endpoint for managing packages. 8 | //! 9 | //! A package is represented by the `Package` struct, which is idempotent. This 10 | //! means you can execute it repeatedly and it'll only run as needed. 11 | 12 | mod providers; 13 | 14 | use command::CommandStatus; 15 | use errors::*; 16 | use futures::{future, Future}; 17 | use host::Host; 18 | use remote::{Request, Response}; 19 | #[doc(hidden)] 20 | pub use self::providers::{factory, PackageProvider, Apt, Dnf, Homebrew, Nix, Pkg, Yum}; 21 | pub use self::providers::Provider; 22 | 23 | /// Represents a system package to be managed for a host. 24 | /// 25 | ///# Example 26 | /// 27 | /// Install a package and print the result. 28 | /// 29 | ///```no_run 30 | ///extern crate futures; 31 | ///extern crate intecture_api; 32 | ///extern crate tokio_core; 33 | /// 34 | ///use futures::{future, Future}; 35 | ///use intecture_api::errors::*; 36 | ///use intecture_api::prelude::*; 37 | ///use tokio_core::reactor::Core; 38 | /// 39 | ///# fn main() { 40 | ///let mut core = Core::new().unwrap(); 41 | ///let handle = core.handle(); 42 | /// 43 | ///let host = Local::new(&handle).wait().unwrap(); 44 | /// 45 | ///let nginx = Package::new(&host, "nginx"); 46 | ///let result = nginx.install().and_then(|status| { 47 | /// match status { 48 | /// // We're performing the install 49 | /// Some(status) => Box::new(status.result().unwrap() 50 | /// .map(|_| println!("Installed")) 51 | /// .map_err(|e| { 52 | /// match *e.kind() { 53 | /// ErrorKind::Command(ref output) => println!("Failed with output: {}", output), 54 | /// _ => unreachable!(), 55 | /// } 56 | /// e 57 | /// })), 58 | /// 59 | /// // This package is already installed 60 | /// None => { 61 | /// println!("Already installed"); 62 | /// Box::new(future::ok(())) as Box> 63 | /// }, 64 | /// } 65 | ///}); 66 | /// 67 | ///core.run(result).unwrap(); 68 | ///# } 69 | ///``` 70 | pub struct Package { 71 | host: H, 72 | provider: Option, 73 | name: String, 74 | } 75 | 76 | impl Package { 77 | /// Create a new `Package` with the default [`Provider`](enum.Provider.html). 78 | pub fn new(host: &H, name: &str) -> Package { 79 | Package { 80 | host: host.clone(), 81 | provider: None, 82 | name: name.into(), 83 | } 84 | } 85 | 86 | /// Create a new `Package` with the specified [`Provider`](enum.Provider.html). 87 | /// 88 | ///## Example 89 | ///``` 90 | ///extern crate futures; 91 | ///extern crate intecture_api; 92 | ///extern crate tokio_core; 93 | /// 94 | ///use futures::Future; 95 | ///use intecture_api::package::Provider; 96 | ///use intecture_api::prelude::*; 97 | ///use tokio_core::reactor::Core; 98 | /// 99 | ///# fn main() { 100 | ///let mut core = Core::new().unwrap(); 101 | ///let handle = core.handle(); 102 | /// 103 | ///let host = Local::new(&handle).wait().unwrap(); 104 | /// 105 | ///Package::with_provider(&host, Provider::Yum, "nginx"); 106 | ///# } 107 | pub fn with_provider(host: &H, provider: Provider, name: &str) -> Package { 108 | Package { 109 | host: host.clone(), 110 | provider: Some(provider), 111 | name: name.into(), 112 | } 113 | } 114 | 115 | /// Check if the package is installed. 116 | pub fn installed(&self) -> Box> { 117 | let request = Request::PackageInstalled(self.provider, self.name.clone()); 118 | Box::new(self.host.request(request) 119 | .chain_err(|| ErrorKind::Request { endpoint: "Package", func: "installed" }) 120 | .map(|msg| { 121 | match msg.into_inner() { 122 | Response::Bool(b) => b, 123 | _ => unreachable!(), 124 | } 125 | })) 126 | } 127 | 128 | /// Install the package. 129 | /// 130 | ///## Idempotence 131 | /// 132 | /// This function is idempotent, which is represented by the type 133 | /// `Future, ...>`. Thus if it returns `Option::None` 134 | /// then the package is already installed, and if it returns `Option::Some` 135 | /// then Intecture is attempting to install the package. 136 | /// 137 | /// If this fn returns `Option::Some<..>`, the nested tuple will hold 138 | /// handles to the live output and the result of the installation. Under 139 | /// the hood this reuses the `Command` endpoint, so see 140 | /// [`Command` docs](../command/struct.Command.html) for detailed 141 | /// usage. 142 | pub fn install(&self) -> Box, Error = Error>> 143 | { 144 | let host = self.host.clone(); 145 | let provider = self.provider; 146 | let name = self.name.clone(); 147 | 148 | Box::new(self.installed() 149 | .and_then(move |installed| { 150 | if installed { 151 | Box::new(future::ok(None)) as Box> 152 | } else { 153 | Box::new(host.request(Request::PackageInstall(provider, name)) 154 | .chain_err(|| ErrorKind::Request { endpoint: "Package", func: "install" }) 155 | .map(|msg| { 156 | Some(CommandStatus::new(msg)) 157 | })) 158 | } 159 | })) 160 | } 161 | 162 | /// Uninstall the package. 163 | /// 164 | ///## Idempotence 165 | /// 166 | /// This function is idempotent, which is represented by the type 167 | /// `Future, ...>`. Thus if it returns `Option::None` 168 | /// then the package is already uninstalled, and if it returns 169 | /// `Option::Some` then Intecture is attempting to uninstall the package. 170 | /// 171 | /// If this fn returns `Option::Some<..>`, the nested tuple will hold 172 | /// handles to the live output and the result of the deinstallation. Under 173 | /// the hood this reuses the `Command` endpoint, so see 174 | /// [`Command` docs](../command/struct.Command.html) for detailed 175 | /// usage. 176 | pub fn uninstall(&self) -> Box, Error = Error>> 177 | { 178 | let host = self.host.clone(); 179 | let provider = self.provider; 180 | let name = self.name.clone(); 181 | 182 | Box::new(self.installed() 183 | .and_then(move |installed| { 184 | if installed { 185 | Box::new(host.request(Request::PackageUninstall(provider, name)) 186 | .chain_err(|| ErrorKind::Request { endpoint: "Package", func: "uninstall" }) 187 | .map(|msg| { 188 | Some(CommandStatus::new(msg)) 189 | })) 190 | } else { 191 | Box::new(future::ok(None)) as Box> 192 | } 193 | })) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /core/src/package/providers/apt.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::process; 14 | use super::PackageProvider; 15 | use telemetry::Os; 16 | use tokio_core::reactor::Handle; 17 | use tokio_process::CommandExt; 18 | use tokio_proto::streaming::Message; 19 | 20 | pub struct Apt; 21 | 22 | impl PackageProvider for Apt { 23 | fn available() -> Result { 24 | Ok(process::Command::new("/usr/bin/type") 25 | .arg("apt-get") 26 | .status() 27 | .chain_err(|| "Could not determine provider availability")? 28 | .success()) 29 | } 30 | 31 | fn installed(&self, handle: &Handle, name: &str, _: &Os) -> ExecutableResult { 32 | let handle = handle.clone(); 33 | let name = name.to_owned(); 34 | 35 | Box::new(process::Command::new("dpkg") 36 | .args(&["--get-selections"]) 37 | .output_async(&handle) 38 | .chain_err(|| "Could not get installed packages") 39 | .and_then(move |output| { 40 | if output.status.success() { 41 | let re = match Regex::new(&format!("(?m){}\\s+install$", name)) { 42 | Ok(r) => r, 43 | Err(e) => return future::err(ErrorKind::Regex(e).into()), 44 | }; 45 | let stdout = String::from_utf8_lossy(&output.stdout); 46 | future::ok( 47 | Message::WithoutBody( 48 | ResponseResult::Ok( 49 | Response::Bool( 50 | re.is_match(&stdout))))) 51 | } else { 52 | future::ok( 53 | Message::WithoutBody( 54 | ResponseResult::Err( 55 | format!("Error running `dpkg --get-selections`: {}", String::from_utf8_lossy(&output.stderr)) 56 | ) 57 | ) 58 | ) 59 | } 60 | })) 61 | } 62 | 63 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 64 | let cmd = match factory() { 65 | Ok(c) => c, 66 | Err(e) => return Box::new(future::ok( 67 | Message::WithoutBody( 68 | ResponseResult::Err( 69 | format!("{}", e.display_chain()))))), 70 | }; 71 | cmd.exec(handle, &["apt-get", "-y", "install", name]) 72 | } 73 | 74 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 75 | let cmd = match factory() { 76 | Ok(c) => c, 77 | Err(e) => return Box::new(future::ok( 78 | Message::WithoutBody( 79 | ResponseResult::Err( 80 | format!("{}", e.display_chain()))))), 81 | }; 82 | cmd.exec(handle, &["apt-get", "-y", "remove", name]) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/package/providers/dnf.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::process; 14 | use super::PackageProvider; 15 | use telemetry::Os; 16 | use tokio_core::reactor::Handle; 17 | use tokio_process::CommandExt; 18 | use tokio_proto::streaming::Message; 19 | 20 | pub struct Dnf; 21 | 22 | impl PackageProvider for Dnf { 23 | fn available() -> Result { 24 | Ok(process::Command::new("/usr/bin/type") 25 | .arg("dnf") 26 | .status() 27 | .chain_err(|| "Could not determine provider availability")? 28 | .success()) 29 | } 30 | 31 | fn installed(&self, handle: &Handle, name: &str, os: &Os) -> ExecutableResult { 32 | let handle = handle.clone(); 33 | let name = name.to_owned(); 34 | let arch = os.arch.clone(); 35 | 36 | Box::new(process::Command::new("dnf") 37 | .args(&["list", "installed"]) 38 | .output_async(&handle) 39 | .chain_err(|| "Could not get installed packages") 40 | .and_then(move |output| { 41 | if output.status.success() { 42 | let re = match Regex::new(&format!("(?m)^{}\\.({}|noarch)\\s+", name, arch)) { 43 | Ok(r) => r, 44 | Err(e) => return future::err(ErrorKind::Regex(e).into()), 45 | }; 46 | let stdout = String::from_utf8_lossy(&output.stdout); 47 | future::ok( 48 | Message::WithoutBody( 49 | ResponseResult::Ok( 50 | Response::Bool( 51 | re.is_match(&stdout))))) 52 | } else { 53 | future::ok( 54 | Message::WithoutBody( 55 | ResponseResult::Err( 56 | format!("Error running `dnf list installed`: {}", String::from_utf8_lossy(&output.stderr)) 57 | ) 58 | ) 59 | ) 60 | } 61 | })) 62 | } 63 | 64 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 65 | let cmd = match factory() { 66 | Ok(c) => c, 67 | Err(e) => return Box::new(future::ok( 68 | Message::WithoutBody( 69 | ResponseResult::Err( 70 | format!("{}", e.display_chain()))))), 71 | }; 72 | cmd.exec(handle, &["dnf", "-y", "install", name]) 73 | } 74 | 75 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 76 | let cmd = match factory() { 77 | Ok(c) => c, 78 | Err(e) => return Box::new(future::ok( 79 | Message::WithoutBody( 80 | ResponseResult::Err( 81 | format!("{}", e.display_chain()))))), 82 | }; 83 | cmd.exec(handle, &["dnf", "-y", "remove", name]) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/package/providers/homebrew.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::process; 14 | use super::PackageProvider; 15 | use telemetry::Os; 16 | use tokio_core::reactor::Handle; 17 | use tokio_process::CommandExt; 18 | use tokio_proto::streaming::Message; 19 | 20 | pub struct Homebrew; 21 | 22 | impl PackageProvider for Homebrew { 23 | fn available() -> Result { 24 | Ok(process::Command::new("/usr/bin/type") 25 | .arg("brew") 26 | .status() 27 | .chain_err(|| "Could not determine provider availability")? 28 | .success()) 29 | } 30 | 31 | fn installed(&self, handle: &Handle, name: &str, _: &Os) -> ExecutableResult { 32 | let handle = handle.clone(); 33 | let name = name.to_owned(); 34 | 35 | Box::new(process::Command::new("brew") 36 | .arg("list") 37 | .output_async(&handle) 38 | .chain_err(|| "Could not get installed packages") 39 | .and_then(move |output| { 40 | if output.status.success() { 41 | let re = match Regex::new(&format!("(?m)(^|\\s+){}\\s+", name)) { 42 | Ok(r) => r, 43 | Err(e) => return future::err(ErrorKind::Regex(e).into()), 44 | }; 45 | let stdout = String::from_utf8_lossy(&output.stdout); 46 | future::ok( 47 | Message::WithoutBody( 48 | ResponseResult::Ok( 49 | Response::Bool( 50 | re.is_match(&stdout))))) 51 | } else { 52 | future::ok( 53 | Message::WithoutBody( 54 | ResponseResult::Err( 55 | format!("Error running `brew list installed`: {}", String::from_utf8_lossy(&output.stderr)) 56 | ) 57 | ) 58 | ) 59 | } 60 | })) 61 | } 62 | 63 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 64 | let cmd = match factory() { 65 | Ok(c) => c, 66 | Err(e) => return Box::new(future::ok( 67 | Message::WithoutBody( 68 | ResponseResult::Err( 69 | format!("{}", e.display_chain()))))), 70 | }; 71 | cmd.exec(handle, &["brew", "install", name]) 72 | } 73 | 74 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 75 | let cmd = match factory() { 76 | Ok(c) => c, 77 | Err(e) => return Box::new(future::ok( 78 | Message::WithoutBody( 79 | ResponseResult::Err( 80 | format!("{}", e.display_chain()))))), 81 | }; 82 | cmd.exec(handle, &["brew", "uninstall", name]) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/package/providers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! OS abstractions for `Package`. 8 | 9 | mod apt; 10 | mod dnf; 11 | mod homebrew; 12 | mod nix; 13 | mod pkg; 14 | mod yum; 15 | 16 | use errors::*; 17 | use remote::ExecutableResult; 18 | pub use self::apt::Apt; 19 | pub use self::dnf::Dnf; 20 | pub use self::homebrew::Homebrew; 21 | pub use self::nix::Nix; 22 | pub use self::pkg::Pkg; 23 | pub use self::yum::Yum; 24 | use telemetry::Os; 25 | use tokio_core::reactor::Handle; 26 | 27 | /// Specific implementation of `Package` 28 | #[derive(Clone, Copy, Serialize, Deserialize)] 29 | pub enum Provider { 30 | Apt, 31 | Dnf, 32 | Homebrew, 33 | Nix, 34 | Pkg, 35 | Yum, 36 | } 37 | 38 | pub trait PackageProvider { 39 | fn available() -> Result where Self: Sized; 40 | fn installed(&self, &Handle, &str, &Os) -> ExecutableResult; 41 | fn install(&self, &Handle, &str) -> ExecutableResult; 42 | fn uninstall(&self, &Handle, &str) -> ExecutableResult; 43 | } 44 | 45 | #[doc(hidden)] 46 | pub fn factory() -> Result> { 47 | if Apt::available()? { 48 | Ok(Box::new(Apt)) 49 | } 50 | else if Dnf::available()? { 51 | Ok(Box::new(Dnf)) 52 | } 53 | else if Homebrew::available()? { 54 | Ok(Box::new(Homebrew)) 55 | } 56 | else if Nix::available()? { 57 | Ok(Box::new(Nix)) 58 | } 59 | else if Pkg::available()? { 60 | Ok(Box::new(Pkg)) 61 | } 62 | else if Yum::available()? { 63 | Ok(Box::new(Yum)) 64 | } else { 65 | Err(ErrorKind::ProviderUnavailable("Package").into()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/package/providers/nix.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::process; 13 | use super::PackageProvider; 14 | use telemetry::Os; 15 | use tokio_core::reactor::Handle; 16 | use tokio_process::CommandExt; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Nix; 20 | 21 | impl PackageProvider for Nix { 22 | fn available() -> Result { 23 | Ok(process::Command::new("/usr/bin/type") 24 | .arg("nix-env") 25 | .status() 26 | .chain_err(|| "Could not determine provider availability")? 27 | .success()) 28 | } 29 | 30 | fn installed(&self, handle: &Handle, name: &str, _: &Os) -> ExecutableResult { 31 | let handle = handle.clone(); 32 | let name = name.to_owned(); 33 | 34 | Box::new(process::Command::new("nix-env") 35 | .args(&["--install", "--dry-run", &name]) 36 | .output_async(&handle) 37 | .chain_err(|| "Could not check if package is installed") 38 | .and_then(move |output| { 39 | if output.status.success() { 40 | let stdout = String::from_utf8_lossy(&output.stdout); 41 | future::ok( 42 | Message::WithoutBody( 43 | ResponseResult::Ok( 44 | Response::Bool( 45 | !stdout.contains("these paths will be fetched"))))) 46 | } else { 47 | future::ok( 48 | Message::WithoutBody( 49 | ResponseResult::Err( 50 | format!("Error running `nix-env --install --dry-run {}`: {}", name, String::from_utf8_lossy(&output.stderr)) 51 | ) 52 | ) 53 | ) 54 | } 55 | })) 56 | } 57 | 58 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 59 | let cmd = match factory() { 60 | Ok(c) => c, 61 | Err(e) => return Box::new(future::ok( 62 | Message::WithoutBody( 63 | ResponseResult::Err( 64 | format!("{}", e.display_chain()))))), 65 | }; 66 | cmd.exec(handle, &["nix-env", "--install", name]) 67 | } 68 | 69 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 70 | let cmd = match factory() { 71 | Ok(c) => c, 72 | Err(e) => return Box::new(future::ok( 73 | Message::WithoutBody( 74 | ResponseResult::Err( 75 | format!("{}", e.display_chain()))))), 76 | }; 77 | cmd.exec(handle, &["nix-env", "--uninstall", name]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/src/package/providers/pkg.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::process; 13 | use super::PackageProvider; 14 | use telemetry::Os; 15 | use tokio_core::reactor::Handle; 16 | use tokio_process::CommandExt; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Pkg; 20 | 21 | impl PackageProvider for Pkg { 22 | fn available() -> Result { 23 | Ok(process::Command::new("/usr/bin/type") 24 | .arg("pkg") 25 | .status() 26 | .chain_err(|| "Could not determine provider availability")? 27 | .success()) 28 | } 29 | 30 | fn installed(&self, handle: &Handle, name: &str, _: &Os) -> ExecutableResult { 31 | let handle = handle.clone(); 32 | let name = name.to_owned(); 33 | 34 | Box::new(process::Command::new("pkg") 35 | .args(&["query", "\"%n\"", &name]) 36 | .output_async(&handle) 37 | .chain_err(|| "Could not get installed packages") 38 | .and_then(move |output| { 39 | future::ok( 40 | Message::WithoutBody( 41 | ResponseResult::Ok( 42 | Response::Bool( 43 | output.status.success())))) 44 | })) 45 | } 46 | 47 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 48 | let cmd = match factory() { 49 | Ok(c) => c, 50 | Err(e) => return Box::new(future::ok( 51 | Message::WithoutBody( 52 | ResponseResult::Err( 53 | format!("{}", e.display_chain()))))), 54 | }; 55 | cmd.exec(handle, &["pkg", "install", "-y", name]) 56 | } 57 | 58 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 59 | let cmd = match factory() { 60 | Ok(c) => c, 61 | Err(e) => return Box::new(future::ok( 62 | Message::WithoutBody( 63 | ResponseResult::Err( 64 | format!("{}", e.display_chain()))))), 65 | }; 66 | cmd.exec(handle, &["pkg", "delete", "-y", name]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/package/providers/yum.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::process; 14 | use super::PackageProvider; 15 | use telemetry::Os; 16 | use tokio_core::reactor::Handle; 17 | use tokio_process::CommandExt; 18 | use tokio_proto::streaming::Message; 19 | 20 | /// The Yum `Package` provider. 21 | pub struct Yum; 22 | 23 | impl PackageProvider for Yum { 24 | fn available() -> Result { 25 | Ok(process::Command::new("/usr/bin/type") 26 | .arg("yum") 27 | .status() 28 | .chain_err(|| "Could not determine provider availability")? 29 | .success()) 30 | } 31 | 32 | fn installed(&self, handle: &Handle, name: &str, os: &Os) -> ExecutableResult { 33 | let handle = handle.clone(); 34 | let name = name.to_owned(); 35 | let arch = os.arch.clone(); 36 | 37 | Box::new(process::Command::new("yum") 38 | .args(&["list", "installed"]) 39 | .output_async(&handle) 40 | .chain_err(|| "Could not get installed packages") 41 | .and_then(move |output| { 42 | if output.status.success() { 43 | let re = match Regex::new(&format!("(?m)^{}\\.({}|noarch)\\s+", name, arch)) { 44 | Ok(r) => r, 45 | Err(e) => return future::err(ErrorKind::Regex(e).into()), 46 | }; 47 | let stdout = String::from_utf8_lossy(&output.stdout); 48 | future::ok( 49 | Message::WithoutBody( 50 | ResponseResult::Ok( 51 | Response::Bool( 52 | re.is_match(&stdout))))) 53 | } else { 54 | future::ok( 55 | Message::WithoutBody( 56 | ResponseResult::Err( 57 | format!("Error running `yum list installed`: {}", String::from_utf8_lossy(&output.stderr)) 58 | ) 59 | ) 60 | ) 61 | } 62 | })) 63 | } 64 | 65 | fn install(&self, handle: &Handle, name: &str) -> ExecutableResult { 66 | let cmd = match factory() { 67 | Ok(c) => c, 68 | Err(e) => return Box::new(future::ok( 69 | Message::WithoutBody( 70 | ResponseResult::Err( 71 | format!("{}", e.display_chain()))))), 72 | }; 73 | cmd.exec(handle, &["yum", "-y", "install", name]) 74 | } 75 | 76 | fn uninstall(&self, handle: &Handle, name: &str) -> ExecutableResult { 77 | let cmd = match factory() { 78 | Ok(c) => c, 79 | Err(e) => return Box::new(future::ok( 80 | Message::WithoutBody( 81 | ResponseResult::Err( 82 | format!("{}", e.display_chain()))))), 83 | }; 84 | cmd.exec(handle, &["yum", "-y", "remove", name]) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/remote.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | // Hopefully in the near future this will be auto-generated. 8 | 9 | use command; 10 | use errors::*; 11 | use futures::{future, Future}; 12 | use host::Host; 13 | use package; 14 | use service; 15 | use std::io; 16 | use telemetry::{self, Telemetry}; 17 | use tokio_proto::streaming::{Body, Message}; 18 | 19 | pub type ExecutableResult = Box, io::Error>>, Error = Error>>; 20 | 21 | #[derive(Serialize, Deserialize)] 22 | pub enum Request { 23 | CommandExec(Option, Vec), 24 | PackageInstalled(Option, String), 25 | PackageInstall(Option, String), 26 | PackageUninstall(Option, String), 27 | ServiceAction(Option, String, String), 28 | ServiceDisable(Option, String), 29 | ServiceEnable(Option, String), 30 | ServiceEnabled(Option, String), 31 | ServiceRunning(Option, String), 32 | TelemetryLoad, 33 | } 34 | 35 | #[derive(Serialize, Deserialize)] 36 | pub enum Response { 37 | Bool(bool), 38 | Null, 39 | TelemetryLoad(telemetry::serializable::Telemetry), 40 | } 41 | 42 | #[derive(Serialize, Deserialize)] 43 | pub enum ResponseResult { 44 | Ok(Response), 45 | Err(String), 46 | } 47 | 48 | pub trait Executable { 49 | fn exec(self, &H) -> ExecutableResult; 50 | } 51 | 52 | impl Executable for Request { 53 | fn exec(self, host: &H) -> ExecutableResult { 54 | match self { 55 | Request::CommandExec(provider, cmd) => { 56 | let provider = match provider { 57 | Some(command::Provider::Generic) => Box::new(command::Generic), 58 | None => match command::factory() { 59 | Ok(p) => p, 60 | Err(e) => return Box::new(future::err(e)), 61 | }, 62 | }; 63 | let args: Vec<&str> = cmd.iter().map(|a| &**a).collect(); 64 | provider.exec(host.handle(), &args) 65 | } 66 | 67 | Request::PackageInstalled(provider, name) => { 68 | let provider = match get_package_provider(provider) { 69 | Ok(p) => p, 70 | Err(e) => return Box::new(future::err(e)), 71 | }; 72 | provider.installed(host.handle(), &name, &host.telemetry().os) 73 | } 74 | 75 | Request::PackageInstall(provider, name) => { 76 | let provider = match get_package_provider(provider) { 77 | Ok(p) => p, 78 | Err(e) => return Box::new(future::err(e)), 79 | }; 80 | provider.install(host.handle(), &name) 81 | } 82 | 83 | Request::PackageUninstall(provider, name) => { 84 | let provider = match get_package_provider(provider) { 85 | Ok(p) => p, 86 | Err(e) => return Box::new(future::err(e)), 87 | }; 88 | provider.uninstall(host.handle(), &name) 89 | } 90 | 91 | Request::ServiceAction(provider, name, action) => { 92 | let provider = match get_service_provider(&host.telemetry(), provider) { 93 | Ok(p) => p, 94 | Err(e) => return Box::new(future::err(e)), 95 | }; 96 | provider.action(host.handle(), &name, &action) 97 | } 98 | 99 | Request::ServiceEnabled(provider, name) => { 100 | let provider = match get_service_provider(&host.telemetry(), provider) { 101 | Ok(p) => p, 102 | Err(e) => return Box::new(future::err(e)), 103 | }; 104 | provider.enabled(host.handle(), &name) 105 | } 106 | 107 | Request::ServiceRunning(provider, name) => { 108 | let provider = match get_service_provider(&host.telemetry(), provider) { 109 | Ok(p) => p, 110 | Err(e) => return Box::new(future::err(e)), 111 | }; 112 | provider.running(host.handle(), &name) 113 | } 114 | 115 | Request::ServiceEnable(provider, name) => { 116 | let provider = match get_service_provider(&host.telemetry(), provider) { 117 | Ok(p) => p, 118 | Err(e) => return Box::new(future::err(e)), 119 | }; 120 | provider.enable(host.handle(), &name) 121 | } 122 | 123 | Request::ServiceDisable(provider, name) => { 124 | let provider = match get_service_provider(&host.telemetry(), provider) { 125 | Ok(p) => p, 126 | Err(e) => return Box::new(future::err(e)), 127 | }; 128 | provider.disable(host.handle(), &name) 129 | } 130 | 131 | Request::TelemetryLoad => { 132 | let provider = match telemetry::factory() { 133 | Ok(p) => p, 134 | Err(e) => return Box::new(future::err(e)), 135 | }; 136 | provider.load() 137 | } 138 | } 139 | } 140 | } 141 | 142 | fn get_package_provider(name: Option) -> Result> { 143 | match name { 144 | Some(package::Provider::Apt) => Ok(Box::new(package::Apt)), 145 | Some(package::Provider::Dnf) => Ok(Box::new(package::Dnf)), 146 | Some(package::Provider::Homebrew) => Ok(Box::new(package::Homebrew)), 147 | Some(package::Provider::Nix) => Ok(Box::new(package::Nix)), 148 | Some(package::Provider::Pkg) => Ok(Box::new(package::Pkg)), 149 | Some(package::Provider::Yum) => Ok(Box::new(package::Yum)), 150 | None => package::factory(), 151 | } 152 | } 153 | 154 | fn get_service_provider(telemetry: &Telemetry, name: Option) -> Result> { 155 | match name { 156 | Some(service::Provider::Debian) => Ok(Box::new(service::Debian)), 157 | Some(service::Provider::Homebrew) => Ok(Box::new(service::Homebrew::new(telemetry))), 158 | Some(service::Provider::Launchctl) => Ok(Box::new(service::Launchctl::new(telemetry))), 159 | Some(service::Provider::Rc) => Ok(Box::new(service::Rc)), 160 | Some(service::Provider::Redhat) => Ok(Box::new(service::Redhat)), 161 | Some(service::Provider::Systemd) => Ok(Box::new(service::Systemd)), 162 | None => service::factory(telemetry), 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /core/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! Endpoint for managing system services. 8 | //! 9 | //! A service is represented by the `Service` struct, which is idempotent. This 10 | //! means you can execute it repeatedly and it'll only run as needed. 11 | 12 | mod providers; 13 | 14 | use command::CommandStatus; 15 | use errors::*; 16 | use futures::{future, Future}; 17 | use host::Host; 18 | use remote::{Request, Response}; 19 | #[doc(hidden)] 20 | pub use self::providers::{ 21 | factory, ServiceProvider, Debian, Homebrew, Launchctl, 22 | Rc, Redhat, Systemd 23 | }; 24 | pub use self::providers::Provider; 25 | 26 | /// Represents a system service to be managed for a host. 27 | /// 28 | ///## Example 29 | /// 30 | /// Enable and start a service. 31 | /// 32 | ///```no_run 33 | ///extern crate futures; 34 | ///extern crate intecture_api; 35 | ///extern crate tokio_core; 36 | /// 37 | ///use futures::{future, Future}; 38 | ///use intecture_api::errors::Error; 39 | ///use intecture_api::prelude::*; 40 | ///use tokio_core::reactor::Core; 41 | /// 42 | ///# fn main() { 43 | ///let mut core = Core::new().unwrap(); 44 | ///let handle = core.handle(); 45 | /// 46 | ///let host = Local::new(&handle).wait().unwrap(); 47 | /// 48 | ///let nginx = Service::new(&host, "nginx"); 49 | ///let result = nginx.enable() 50 | /// .and_then(|_| { 51 | /// nginx.action("start") 52 | /// .and_then(|maybe_status| { 53 | /// match maybe_status { 54 | /// Some(status) => Box::new(status.result().unwrap().map(|_| ())) as Box>, 55 | /// None => Box::new(future::ok(())), 56 | /// } 57 | /// }) 58 | /// }); 59 | /// 60 | ///core.run(result).unwrap(); 61 | ///# } 62 | ///``` 63 | pub struct Service { 64 | host: H, 65 | provider: Option, 66 | name: String, 67 | } 68 | 69 | impl Service { 70 | /// Create a new `Service` with the default [`Provider`](enum.Provider.html). 71 | pub fn new(host: &H, name: &str) -> Service { 72 | Service { 73 | host: host.clone(), 74 | provider: None, 75 | name: name.into(), 76 | } 77 | } 78 | 79 | /// Create a new `Service` with the specified [`Provider`](enum.Provider.html). 80 | /// 81 | ///## Example 82 | ///``` 83 | ///extern crate futures; 84 | ///extern crate intecture_api; 85 | ///extern crate tokio_core; 86 | /// 87 | ///use futures::Future; 88 | ///use intecture_api::service::Provider; 89 | ///use intecture_api::prelude::*; 90 | ///use tokio_core::reactor::Core; 91 | /// 92 | ///# fn main() { 93 | ///let mut core = Core::new().unwrap(); 94 | ///let handle = core.handle(); 95 | /// 96 | ///let host = Local::new(&handle).wait().unwrap(); 97 | /// 98 | ///Service::with_provider(&host, Provider::Systemd, "nginx"); 99 | ///# } 100 | pub fn with_provider(host: &H, provider: Provider, name: &str) -> Service { 101 | Service { 102 | host: host.clone(), 103 | provider: Some(provider), 104 | name: name.into(), 105 | } 106 | } 107 | 108 | /// Check if the service is currently running. 109 | pub fn running(&self) -> Box> { 110 | let request = Request::ServiceRunning(self.provider, self.name.clone()); 111 | Box::new(self.host.request(request) 112 | .chain_err(|| ErrorKind::Request { endpoint: "Service", func: "running" }) 113 | .map(|msg| { 114 | match msg.into_inner() { 115 | Response::Bool(b) => b, 116 | _ => unreachable!(), 117 | } 118 | })) 119 | } 120 | 121 | /// Perform an action for the service, e.g. "start". 122 | /// 123 | ///## Cross-platform services 124 | /// 125 | /// By design, actions are specific to a particular service and are not 126 | /// cross-platform. Actions are defined by the package maintainer that 127 | /// wrote the service configuration, thus users should take care that they 128 | /// adhere to the configuration for each platform they target. 129 | /// 130 | ///## Idempotence 131 | /// 132 | /// This function is idempotent when running either the "start" or "stop" 133 | /// actions, as it will check first whether the service is already running. 134 | /// Idempotence is represented by the type `Future, ...>`. 135 | /// Thus if it returns `Option::None` then the service is already in the 136 | /// required state, and if it returns `Option::Some` then Intecture is 137 | /// attempting to transition the service to the required state. 138 | /// 139 | /// If this fn returns `Option::Some<..>`, the nested tuple will hold 140 | /// handles to the live output and the result of the action. Under the hood 141 | /// this reuses the `Command` endpoint, so see 142 | /// [`Command` docs](../command/struct.Command.html) for detailed 143 | /// usage. 144 | pub fn action(&self, action: &str) -> Box, Error = Error>> 145 | { 146 | if action == "start" || action == "stop" { 147 | let host = self.host.clone(); 148 | let name = self.name.clone(); 149 | let provider = self.provider; 150 | let action = action.to_owned(); 151 | 152 | Box::new(self.running() 153 | .and_then(move |running| { 154 | if (running && action == "start") || (!running && action == "stop") { 155 | Box::new(future::ok(None)) as Box> 156 | } else { 157 | Self::do_action(&host, provider, &name, &action) 158 | } 159 | })) 160 | } else { 161 | Self::do_action(&self.host, self.provider, &self.name, action) 162 | } 163 | } 164 | 165 | fn do_action(host: &H, provider: Option, name: &str, action: &str) 166 | -> Box, Error = Error>> 167 | { 168 | let request = Request::ServiceAction(provider, name.into(), action.into()); 169 | Box::new(host.request(request) 170 | .chain_err(|| ErrorKind::Request { endpoint: "Service", func: "action" }) 171 | .map(|msg| Some(CommandStatus::new(msg)))) 172 | } 173 | 174 | /// Check if the service will start at boot. 175 | pub fn enabled(&self) -> Box> { 176 | let request = Request::ServiceEnabled(self.provider, self.name.clone()); 177 | Box::new(self.host.request(request) 178 | .chain_err(|| ErrorKind::Request { endpoint: "Service", func: "enabled" }) 179 | .map(|msg| { 180 | match msg.into_inner() { 181 | Response::Bool(b) => b, 182 | _ => unreachable!(), 183 | } 184 | })) 185 | } 186 | 187 | /// Instruct the service to start at boot. 188 | /// 189 | ///## Idempotence 190 | /// 191 | /// This function is idempotent, which is represented by the type 192 | /// `Future, ...>`. Thus if it returns `Option::None` 193 | /// then the service is already enabled, and if it returns `Option::Some` 194 | /// then Intecture is attempting to enable the service. 195 | /// 196 | /// If this fn returns `Option::Some<..>`, the nested tuple will hold 197 | /// handles to the live output and the 'enable' command result. Under 198 | /// the hood this reuses the `Command` endpoint, so see 199 | /// [`Command` docs](../command/struct.Command.html) for detailed 200 | /// usage. 201 | pub fn enable(&self) -> Box, Error = Error>> 202 | { 203 | let host = self.host.clone(); 204 | let provider = self.provider; 205 | let name = self.name.clone(); 206 | 207 | Box::new(self.enabled() 208 | .and_then(move |enabled| { 209 | if enabled { 210 | Box::new(future::ok(None)) as Box> 211 | } else { 212 | let request = Request::ServiceEnable(provider, name); 213 | Box::new(host.request(request) 214 | .chain_err(|| ErrorKind::Request { endpoint: "Service", func: "enable" }) 215 | .map(|msg| match msg.into_inner() { 216 | Response::Null => Some(()), 217 | _ => unreachable!(), 218 | })) 219 | } 220 | })) 221 | } 222 | 223 | /// Prevent the service from starting at boot. 224 | /// 225 | ///## Idempotence 226 | /// 227 | /// This function is idempotent, which is represented by the type 228 | /// `Future, ...>`. Thus if it returns `Option::None` 229 | /// then the service is already disabled, and if it returns `Option::Some` 230 | /// then Intecture is attempting to disable the service. 231 | /// 232 | /// If this fn returns `Option::Some<..>`, the nested tuple will hold 233 | /// handles to the live output and the 'disable' command result. Under 234 | /// the hood this reuses the `Command` endpoint, so see 235 | /// [`Command` docs](../command/struct.Command.html) for detailed 236 | /// usage. 237 | pub fn disable(&self) -> Box, Error = Error>> 238 | { 239 | let host = self.host.clone(); 240 | let provider = self.provider; 241 | let name = self.name.clone(); 242 | 243 | Box::new(self.enabled() 244 | .and_then(move |enabled| { 245 | if enabled { 246 | let request = Request::ServiceDisable(provider, name); 247 | Box::new(host.request(request) 248 | .chain_err(|| ErrorKind::Request { endpoint: "Service", func: "disable" }) 249 | .map(|msg| match msg.into_inner() { 250 | Response::Null => Some(()), 251 | _ => unreachable!(), 252 | })) 253 | } else { 254 | Box::new(future::ok(None)) as Box> 255 | } 256 | })) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /core/src/service/providers/debian.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::fs::read_dir; 14 | use std::process; 15 | use super::ServiceProvider; 16 | use telemetry::{LinuxDistro, OsFamily, Telemetry}; 17 | use tokio_core::reactor::Handle; 18 | use tokio_process::CommandExt; 19 | use tokio_proto::streaming::Message; 20 | 21 | pub struct Debian; 22 | 23 | impl ServiceProvider for Debian { 24 | fn available(telemetry: &Telemetry) -> Result { 25 | Ok(telemetry.os.family == OsFamily::Linux(LinuxDistro::Debian)) 26 | } 27 | 28 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 29 | let status = match process::Command::new("service") 30 | .args(&[name, "status"]) 31 | .status_async2(handle) 32 | .chain_err(|| "Error checking if service is running") 33 | { 34 | Ok(s) => s, 35 | Err(e) => return Box::new(future::err(e)), 36 | }; 37 | Box::new(status.map(|s| Message::WithoutBody( 38 | ResponseResult::Ok( 39 | Response::Bool(s.success())))) 40 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("service status")))) 41 | } 42 | 43 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 44 | let cmd = match factory() { 45 | Ok(c) => c, 46 | Err(e) => return Box::new(future::ok( 47 | Message::WithoutBody( 48 | ResponseResult::Err( 49 | format!("{}", e.display_chain()))))), 50 | }; 51 | cmd.exec(handle, &["service", action, name]) 52 | } 53 | 54 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 55 | let name = name.to_owned(); 56 | 57 | Box::new(process::Command::new("/sbin/runlevel") 58 | .output_async(&handle) 59 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("/sbin/runlevel"))) 60 | .and_then(move |output| { 61 | if output.status.success() { 62 | let mut stdout = (*String::from_utf8_lossy(&output.stdout)).to_owned(); 63 | let runlevel = match stdout.pop() { 64 | Some(c) => c, 65 | None => return future::ok(Message::WithoutBody(ResponseResult::Err("Could not determine current runlevel".into()))), 66 | }; 67 | 68 | let dir = match read_dir(&format!("/etc/rc{}.d", runlevel)) { 69 | Ok(dir) => dir, 70 | Err(e) => return future::err(Error::with_chain(e, ErrorKind::Msg("Could not read rc dir".into()))), 71 | }; 72 | 73 | let regex = match Regex::new(&format!("/S[0-9]+{}$", name)) { 74 | Ok(r) => r, 75 | Err(e) => return future::err(Error::with_chain(e, ErrorKind::Msg("Could not create Debian::enabled regex".into()))), 76 | }; 77 | let mut enabled = false; 78 | for file in dir { 79 | if let Ok(file) = file { 80 | if regex.is_match(&file.file_name().to_string_lossy()) { 81 | enabled = true; 82 | break; 83 | } 84 | } 85 | } 86 | 87 | future::ok(Message::WithoutBody(ResponseResult::Ok(Response::Bool(enabled)))) 88 | } else { 89 | future::err(ErrorKind::SystemCommand("/usr/bin/runlevel").into()) 90 | } 91 | })) 92 | } 93 | 94 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 95 | Box::new(process::Command::new("/usr/sbin/update-rc.d") 96 | .args(&["enable", name]) 97 | .output_async(handle) 98 | .map(|out| { 99 | if out.status.success() { 100 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 101 | } else { 102 | Message::WithoutBody(ResponseResult::Err( 103 | format!("Could not enable service: {}", String::from_utf8_lossy(&out.stderr)))) 104 | } 105 | }) 106 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("update-rc.d enable ")))) 107 | } 108 | 109 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 110 | Box::new(process::Command::new("/usr/sbin/update-rc.d") 111 | .args(&["disable", name]) 112 | .output_async(handle) 113 | .map(|out| { 114 | if out.status.success() { 115 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 116 | } else { 117 | Message::WithoutBody(ResponseResult::Err( 118 | format!("Could not disable service: {}", String::from_utf8_lossy(&out.stderr)))) 119 | } 120 | }) 121 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("update-rc.d disable ")))) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /core/src/service/providers/homebrew.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use error_chain::ChainedError; 8 | use errors::*; 9 | use futures::future; 10 | use remote::{ExecutableResult, ResponseResult}; 11 | use std::process; 12 | use super::{Launchctl, ServiceProvider}; 13 | use telemetry::Telemetry; 14 | use tokio_core::reactor::Handle; 15 | use tokio_proto::streaming::Message; 16 | 17 | pub struct Homebrew { 18 | inner: Launchctl, 19 | } 20 | 21 | impl Homebrew { 22 | #[doc(hidden)] 23 | pub fn new(telemetry: &Telemetry) -> Homebrew { 24 | Homebrew { 25 | inner: Launchctl::new(telemetry), 26 | } 27 | } 28 | } 29 | 30 | impl ServiceProvider for Homebrew { 31 | fn available(telemetry: &Telemetry) -> Result { 32 | let brew = process::Command::new("/usr/bin/type") 33 | .arg("brew") 34 | .status() 35 | .chain_err(|| "Could not determine provider availability")? 36 | .success(); 37 | 38 | Ok(brew && Launchctl::available(telemetry)?) 39 | } 40 | 41 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 42 | self.inner.running(handle, name) 43 | } 44 | 45 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 46 | // @todo This isn't the most reliable method. Ideally a user would 47 | // invoke these commands themselves. 48 | let result = if action == "stop" { 49 | self.inner.uninstall_plist(name) 50 | } else { 51 | let path = format!("/usr/local/opt/{}/homebrew.mxcl.{0}.plist", name); 52 | self.inner.install_plist(path) 53 | }; 54 | 55 | if let Err(e) = result { 56 | return Box::new(future::ok( 57 | Message::WithoutBody( 58 | ResponseResult::Err( 59 | format!("{}", e.display_chain()))))) 60 | } 61 | 62 | self.inner.action(handle, name, action) 63 | } 64 | 65 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 66 | self.inner.enabled(handle, name) 67 | } 68 | 69 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 70 | self.inner.enable(handle, name) 71 | } 72 | 73 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 74 | self.inner.disable(handle, name) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/service/providers/launchctl.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::{fs, process}; 14 | use std::path::{Path, PathBuf}; 15 | use super::ServiceProvider; 16 | use telemetry::{OsFamily, Telemetry}; 17 | use tokio_core::reactor::Handle; 18 | use tokio_process::CommandExt; 19 | use tokio_proto::streaming::Message; 20 | 21 | pub struct Launchctl { 22 | domain_target: String, 23 | service_path: PathBuf, 24 | } 25 | 26 | impl Launchctl { 27 | #[doc(hidden)] 28 | pub fn new(telemetry: &Telemetry) -> Launchctl { 29 | let (domain_target, service_path) = if telemetry.user.is_root() { 30 | ("system".into(), "/Library/LaunchDaemons".into()) 31 | } else { 32 | let mut path = telemetry.user.home_dir.clone(); 33 | path.push("Library/LaunchAgents"); 34 | (format!("gui/{}", telemetry.user.uid), path) 35 | }; 36 | 37 | Launchctl { domain_target, service_path } 38 | } 39 | 40 | #[doc(hidden)] 41 | pub fn install_plist>(&self, path: P) -> Result<()> { 42 | if let Some(name) = path.as_ref().file_name() { 43 | let mut install_path = self.service_path.clone(); 44 | 45 | // Create `Launch..` dir if it doesn't already exist. 46 | if !install_path.exists() { 47 | fs::create_dir(&install_path)?; 48 | } 49 | 50 | install_path.push(name); 51 | 52 | if !install_path.exists() { 53 | fs::copy(&path, &self.service_path) 54 | .chain_err(|| "Could not install plist")?; 55 | } 56 | 57 | Ok(()) 58 | } else { 59 | Err("Plist path does not contain filename".into()) 60 | } 61 | } 62 | 63 | #[doc(hidden)] 64 | pub fn uninstall_plist(&self, name: &str) -> Result<()> { 65 | let mut path = self.service_path.clone(); 66 | path.push(name); 67 | path.set_extension("plist"); 68 | if path.exists() { 69 | fs::remove_file(&path) 70 | .chain_err(|| "Could not uninstall plist")?; 71 | } 72 | 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl ServiceProvider for Launchctl { 78 | fn available(telemetry: &Telemetry) -> Result { 79 | Ok(telemetry.os.family == OsFamily::Darwin && telemetry.os.version_min >= 11) 80 | } 81 | 82 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 83 | let status = match process::Command::new("/bin/launchctl") 84 | .args(&["blame", &format!("{}/{}", self.domain_target, name)]) 85 | .status_async2(handle) 86 | .chain_err(|| "Error checking if service is running") 87 | { 88 | Ok(s) => s, 89 | Err(e) => return Box::new(future::err(e)), 90 | }; 91 | Box::new(status.map(|s| Message::WithoutBody( 92 | ResponseResult::Ok( 93 | Response::Bool(s.success())))) 94 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("launchctl blame")))) 95 | } 96 | 97 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 98 | let action = match action { 99 | "start" => "bootstrap", 100 | "stop" => "bootout", 101 | "restart" => "kickstart -k", 102 | _ => action, 103 | }; 104 | 105 | let cmd = match factory() { 106 | Ok(c) => c, 107 | Err(e) => return Box::new(future::ok( 108 | Message::WithoutBody( 109 | ResponseResult::Err( 110 | format!("{}", e.display_chain()))))), 111 | }; 112 | 113 | // Run through shell as `action` may contain multiple args with spaces. 114 | // If we passed `action` as a single argument, it would automatically 115 | // be quoted and multiple args would appear as a single quoted arg. 116 | cmd.exec(handle, &[ 117 | "/bin/sh", 118 | "-c", 119 | &format!("/bin/launchctl {} {} {}/{}.plist", action, self.domain_target, self.service_path.display(), name) 120 | ]) 121 | } 122 | 123 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 124 | let name = name.to_owned(); 125 | 126 | Box::new(process::Command::new("/bin/launchctl") 127 | .args(&["print-disabled", &self.domain_target]) 128 | .output_async(handle) 129 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("launchctl print-disabled "))) 130 | .and_then(move |out| { 131 | if out.status.success() { 132 | let re = match Regex::new(&format!("^\\s+\"{}\" => false", name)) { 133 | Ok(r) => r, 134 | Err(e) => return future::err(Error::with_chain(e, ErrorKind::Msg("Could not create Launchctl::enabled Regex".into()))) 135 | }; 136 | let stdout = String::from_utf8_lossy(&out.stdout); 137 | let is_match = !re.is_match(&stdout); 138 | 139 | future::ok(Message::WithoutBody(ResponseResult::Ok(Response::Bool(is_match)))) 140 | } else { 141 | future::err(ErrorKind::SystemCommand("/bin/launchctl").into()) 142 | } 143 | })) 144 | } 145 | 146 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 147 | Box::new(process::Command::new("/bin/launchctl") 148 | .args(&["enable", &format!("{}/{}", self.domain_target, name)]) 149 | .output_async(handle) 150 | .map(|out| { 151 | if out.status.success() { 152 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 153 | } else { 154 | Message::WithoutBody(ResponseResult::Err( 155 | format!("Could not enable service: {}", String::from_utf8_lossy(&out.stderr)))) 156 | } 157 | }) 158 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("launchctl enable ")))) 159 | } 160 | 161 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 162 | Box::new(process::Command::new("/bin/launchctl") 163 | .args(&["disable", &format!("{}/{}", self.domain_target, name)]) 164 | .output_async(handle) 165 | .map(|out| { 166 | if out.status.success() { 167 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 168 | } else { 169 | Message::WithoutBody(ResponseResult::Err( 170 | format!("Could not disable service: {}", String::from_utf8_lossy(&out.stderr)))) 171 | } 172 | }) 173 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("launchctl disable ")))) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /core/src/service/providers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! OS abstractions for `Service`. 8 | 9 | mod debian; 10 | mod homebrew; 11 | mod launchctl; 12 | mod rc; 13 | mod redhat; 14 | mod systemd; 15 | 16 | use errors::*; 17 | use remote::ExecutableResult; 18 | pub use self::debian::Debian; 19 | pub use self::homebrew::Homebrew; 20 | pub use self::launchctl::Launchctl; 21 | pub use self::rc::Rc; 22 | pub use self::redhat::Redhat; 23 | pub use self::systemd::Systemd; 24 | use telemetry::Telemetry; 25 | use tokio_core::reactor::Handle; 26 | 27 | /// Specific implementation of `Service` 28 | #[derive(Clone, Copy, Serialize, Deserialize)] 29 | pub enum Provider { 30 | Debian, 31 | Homebrew, 32 | Launchctl, 33 | Rc, 34 | Redhat, 35 | Systemd, 36 | } 37 | 38 | pub trait ServiceProvider { 39 | fn available(&Telemetry) -> Result where Self: Sized; 40 | fn running(&self, &Handle, &str) -> ExecutableResult; 41 | fn action(&self, &Handle, &str, &str) -> ExecutableResult; 42 | fn enabled(&self, &Handle, &str) -> ExecutableResult; 43 | fn enable(&self, &Handle, &str) -> ExecutableResult; 44 | fn disable(&self, &Handle, &str) -> ExecutableResult; 45 | } 46 | 47 | #[doc(hidden)] 48 | pub fn factory(telemetry: &Telemetry) -> Result> { 49 | if Systemd::available(telemetry)? { 50 | Ok(Box::new(Systemd)) 51 | } else if Debian::available(telemetry)? { 52 | Ok(Box::new(Debian)) 53 | } else if Homebrew::available(telemetry)? { 54 | Ok(Box::new(Homebrew::new(telemetry))) 55 | } else if Launchctl::available(telemetry)? { 56 | Ok(Box::new(Launchctl::new(telemetry))) 57 | } else if Rc::available(telemetry)? { 58 | Ok(Box::new(Rc)) 59 | } else if Redhat::available(telemetry)? { 60 | Ok(Box::new(Redhat)) 61 | } else { 62 | Err(ErrorKind::ProviderUnavailable("Service").into()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/service/providers/rc.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use regex::Regex; 12 | use remote::{ExecutableResult, Response, ResponseResult}; 13 | use std::process; 14 | use super::ServiceProvider; 15 | use telemetry::{OsFamily, Telemetry}; 16 | use tokio_core::reactor::Handle; 17 | use tokio_process::CommandExt; 18 | use tokio_proto::streaming::Message; 19 | 20 | pub struct Rc; 21 | 22 | impl ServiceProvider for Rc { 23 | fn available(telemetry: &Telemetry) -> Result { 24 | Ok(telemetry.os.family == OsFamily::Bsd) 25 | } 26 | 27 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 28 | let status = match process::Command::new("service") 29 | .args(&[name, "status"]) 30 | .status_async2(handle) 31 | .chain_err(|| "Error checking if service is running") 32 | { 33 | Ok(s) => s, 34 | Err(e) => return Box::new(future::err(e)), 35 | }; 36 | Box::new(status.map(|s| Message::WithoutBody( 37 | ResponseResult::Ok( 38 | Response::Bool(s.success())))) 39 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("service status")))) 40 | } 41 | 42 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 43 | let cmd = match factory() { 44 | Ok(c) => c, 45 | Err(e) => return Box::new(future::ok( 46 | Message::WithoutBody( 47 | ResponseResult::Err( 48 | format!("{}", e.display_chain()))))), 49 | }; 50 | cmd.exec(handle, &["service", action, name]) 51 | } 52 | 53 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 54 | let name = name.to_owned(); 55 | 56 | Box::new(process::Command::new("/usr/sbin/sysrc") 57 | .arg(&format!("{}_enable", name)) // XXX Assuming "_enable" is the correct suffix 58 | .output_async(&handle) 59 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("/usr/sbin/sysrc _enable"))) 60 | .and_then(move |output| { 61 | if output.status.success() { 62 | let re = match Regex::new(&format!("^{}_enable: (?i:no)", name)) { 63 | Ok(r) => r, 64 | Err(e) => return future::err(Error::with_chain(e, ErrorKind::Msg("Could not create Rc::enabled Regex".into()))) 65 | }; 66 | let stdout = String::from_utf8_lossy(&output.stdout); 67 | 68 | // XXX Assuming anything other than "no" is enabled 69 | let is_match = !re.is_match(&stdout); 70 | 71 | future::ok(Message::WithoutBody(ResponseResult::Ok(Response::Bool(is_match)))) 72 | } else { 73 | future::ok(Message::WithoutBody(ResponseResult::Ok(Response::Bool(false)))) 74 | } 75 | })) 76 | } 77 | 78 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 79 | Box::new(process::Command::new("/usr/sbin/sysrc") 80 | .arg(&format!("{}_enable=\"YES\"", name)) 81 | .output_async(handle) 82 | .map(|out| { 83 | if out.status.success() { 84 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 85 | } else { 86 | Message::WithoutBody(ResponseResult::Err( 87 | format!("Could not enable service: {}", String::from_utf8_lossy(&out.stderr)))) 88 | } 89 | }) 90 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl enable ")))) 91 | } 92 | 93 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 94 | Box::new(process::Command::new("/usr/sbin/sysrc") 95 | .arg(&format!("{}_enable=\"NO\"", name)) 96 | .output_async(handle) 97 | .map(|out| { 98 | if out.status.success() { 99 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 100 | } else { 101 | Message::WithoutBody(ResponseResult::Err( 102 | format!("Could not disable service: {}", String::from_utf8_lossy(&out.stderr)))) 103 | } 104 | }) 105 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl disable ")))) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/service/providers/redhat.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::process; 13 | use super::ServiceProvider; 14 | use telemetry::{LinuxDistro, OsFamily, Telemetry}; 15 | use tokio_core::reactor::Handle; 16 | use tokio_process::CommandExt; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Redhat; 20 | 21 | impl ServiceProvider for Redhat { 22 | fn available(telemetry: &Telemetry) -> Result { 23 | Ok(telemetry.os.family == OsFamily::Linux(LinuxDistro::RHEL)) 24 | } 25 | 26 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 27 | let status = match process::Command::new("service") 28 | .args(&[name, "status"]) 29 | .status_async2(handle) 30 | .chain_err(|| "Error checking if service is running") 31 | { 32 | Ok(s) => s, 33 | Err(e) => return Box::new(future::err(e)), 34 | }; 35 | Box::new(status.map(|s| Message::WithoutBody( 36 | ResponseResult::Ok( 37 | Response::Bool(s.success())))) 38 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("service status")))) 39 | } 40 | 41 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 42 | let cmd = match factory() { 43 | Ok(c) => c, 44 | Err(e) => return Box::new(future::ok( 45 | Message::WithoutBody( 46 | ResponseResult::Err( 47 | format!("{}", e.display_chain()))))), 48 | }; 49 | cmd.exec(handle, &["service", action, name]) 50 | } 51 | 52 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 53 | let status = match process::Command::new("/usr/sbin/chkconfig") 54 | .arg(name) 55 | .status_async2(handle) 56 | .chain_err(|| "Error checking if service is enabled") 57 | { 58 | Ok(s) => s, 59 | Err(e) => return Box::new(future::err(e)), 60 | }; 61 | Box::new(status.map(|s| Message::WithoutBody( 62 | ResponseResult::Ok( 63 | Response::Bool(s.success())))) 64 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("chkconfig ")))) 65 | } 66 | 67 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 68 | Box::new(process::Command::new("/usr/sbin/chkconfig") 69 | .args(&[name, "on"]) 70 | .output_async(handle) 71 | .map(|out| { 72 | if out.status.success() { 73 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 74 | } else { 75 | Message::WithoutBody(ResponseResult::Err( 76 | format!("Could not enable service: {}", String::from_utf8_lossy(&out.stderr)))) 77 | } 78 | }) 79 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("chkconfig on")))) 80 | } 81 | 82 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 83 | Box::new(process::Command::new("/usr/sbin/chkconfig") 84 | .args(&[name, "off"]) 85 | .output_async(handle) 86 | .map(|out| { 87 | if out.status.success() { 88 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 89 | } else { 90 | Message::WithoutBody(ResponseResult::Err( 91 | format!("Could not disable service: {}", String::from_utf8_lossy(&out.stderr)))) 92 | } 93 | }) 94 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("chkconfig off")))) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/service/providers/systemd.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use command::factory; 8 | use error_chain::ChainedError; 9 | use errors::*; 10 | use futures::{future, Future}; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::process; 13 | use super::ServiceProvider; 14 | use telemetry::Telemetry; 15 | use tokio_core::reactor::Handle; 16 | use tokio_process::CommandExt; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Systemd; 20 | 21 | impl ServiceProvider for Systemd { 22 | fn available(_: &Telemetry) -> Result { 23 | let output = process::Command::new("/usr/bin/stat") 24 | .args(&["--format=%N", "/proc/1/exe"]) 25 | .output() 26 | .chain_err(|| "Could not determine provider availability")?; 27 | 28 | if output.status.success() { 29 | let out = String::from_utf8_lossy(&output.stdout); 30 | Ok(out.contains("systemd")) 31 | } else { 32 | Err(ErrorKind::SystemCommand("/usr/bin/stat").into()) 33 | } 34 | } 35 | 36 | fn running(&self, handle: &Handle, name: &str) -> ExecutableResult { 37 | let status = match process::Command::new("systemctl") 38 | .args(&["is-active", name]) 39 | .status_async2(handle) 40 | .chain_err(|| "Error checking if service is running") 41 | { 42 | Ok(s) => s, 43 | Err(e) => return Box::new(future::err(e)), 44 | }; 45 | Box::new(status.map(|s| Message::WithoutBody( 46 | ResponseResult::Ok( 47 | Response::Bool(s.success())))) 48 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl is-active")))) 49 | } 50 | 51 | fn action(&self, handle: &Handle, name: &str, action: &str) -> ExecutableResult { 52 | let cmd = match factory() { 53 | Ok(c) => c, 54 | Err(e) => return Box::new(future::ok( 55 | Message::WithoutBody( 56 | ResponseResult::Err( 57 | format!("{}", e.display_chain()))))), 58 | }; 59 | cmd.exec(handle, &["systemctl", action, name]) 60 | } 61 | 62 | fn enabled(&self, handle: &Handle, name: &str) -> ExecutableResult { 63 | let status = match process::Command::new("systemctl") 64 | .args(&["is-enabled", name]) 65 | .status_async2(handle) 66 | .chain_err(|| "Error checking if service is enabled") 67 | { 68 | Ok(s) => s, 69 | Err(e) => return Box::new(future::err(e)), 70 | }; 71 | Box::new(status.map(|s| Message::WithoutBody( 72 | ResponseResult::Ok( 73 | Response::Bool(s.success())))) 74 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl is-enabled")))) 75 | } 76 | 77 | fn enable(&self, handle: &Handle, name: &str) -> ExecutableResult { 78 | Box::new(process::Command::new("systemctl") 79 | .args(&["enable", name]) 80 | .output_async(handle) 81 | .map(|out| { 82 | if out.status.success() { 83 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 84 | } else { 85 | Message::WithoutBody(ResponseResult::Err( 86 | format!("Could not enable service: {}", String::from_utf8_lossy(&out.stderr)))) 87 | } 88 | }) 89 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl enable ")))) 90 | } 91 | 92 | fn disable(&self, handle: &Handle, name: &str) -> ExecutableResult { 93 | Box::new(process::Command::new("systemctl") 94 | .args(&["disable", name]) 95 | .output_async(handle) 96 | .map(|out| { 97 | if out.status.success() { 98 | Message::WithoutBody(ResponseResult::Ok(Response::Null)) 99 | } else { 100 | Message::WithoutBody(ResponseResult::Err( 101 | format!("Could not disable service: {}", String::from_utf8_lossy(&out.stderr)))) 102 | } 103 | }) 104 | .map_err(|e| Error::with_chain(e, ErrorKind::SystemCommand("systemctl disable ")))) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /core/src/target/default.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use hostname::get_hostname; 9 | use regex::Regex; 10 | use std::process; 11 | use telemetry::{FsMount, User}; 12 | use users::{get_group_by_gid, get_user_by_uid, get_current_uid}; 13 | use users::os::unix::UserExt; 14 | 15 | pub fn hostname() -> Result { 16 | match get_hostname() { 17 | Some(name) => Ok(name), 18 | None => Err("Could not determine hostname".into()), 19 | } 20 | } 21 | 22 | pub enum FsFieldOrder { 23 | Filesystem, 24 | Size, 25 | Used, 26 | Available, 27 | Capacity, 28 | Mount, 29 | Blank, 30 | } 31 | 32 | pub fn fs() -> Result> { 33 | self::parse_fs(&[ 34 | self::FsFieldOrder::Filesystem, 35 | self::FsFieldOrder::Size, 36 | self::FsFieldOrder::Used, 37 | self::FsFieldOrder::Available, 38 | self::FsFieldOrder::Capacity, 39 | self::FsFieldOrder::Mount, 40 | ]) 41 | } 42 | 43 | pub fn parse_fs(fields: &[FsFieldOrder]) -> Result> { 44 | let mount_out = process::Command::new("df") 45 | .arg("-Pk") 46 | .output() 47 | .chain_err(|| ErrorKind::SystemCommand("sysctl"))?; 48 | let mount = String::from_utf8(mount_out.stdout).chain_err(|| ErrorKind::SystemCommandOutput("sysctl"))?; 49 | 50 | let mut pattern = "(?m)^".to_string(); 51 | 52 | for field in fields { 53 | match *field { 54 | FsFieldOrder::Filesystem => pattern.push_str("(?P.+?)"), 55 | FsFieldOrder::Size => pattern.push_str("(?P[0-9]+)"), 56 | FsFieldOrder::Used => pattern.push_str("(?P[0-9]+)"), 57 | FsFieldOrder::Available => pattern.push_str("(?P[0-9]+)"), 58 | FsFieldOrder::Capacity => pattern.push_str("(?P[0-9]{1,3})%"), 59 | FsFieldOrder::Mount => pattern.push_str("(?P/.*)"), 60 | FsFieldOrder::Blank => pattern.push_str(r"[^\s]+"), 61 | } 62 | 63 | pattern.push_str(r"[\s]*"); 64 | } 65 | 66 | pattern.push_str("$"); 67 | 68 | let regex = Regex::new(&pattern).unwrap(); 69 | let mut fs = vec!(); 70 | 71 | let lines: Vec<&str> = mount.lines().collect(); 72 | for line in lines { 73 | if let Some(cap) = regex.captures(line) { 74 | fs.push(FsMount { 75 | filesystem: cap.name("fs").unwrap().as_str().to_string(), 76 | mountpoint: cap.name("mount").unwrap().as_str().to_string(), 77 | size: cap.name("size").unwrap().as_str().parse::() 78 | .chain_err(|| format!("could not discern {} from sysctl output", "size of mount"))?, 79 | used: cap.name("used").unwrap().as_str().parse::() 80 | .chain_err(|| format!("could not discern {} from sysctl output", "used space"))?, 81 | available: cap.name("available").unwrap().as_str().parse::() 82 | .chain_err(|| format!("could not discern {} from sysctl output", "available space"))?, 83 | capacity: cap.name("capacity").unwrap().as_str().parse::() 84 | .chain_err(|| format!("could not discern {} from sysctl output", "mount capacity"))? / 100f32, 85 | }); 86 | } 87 | }; 88 | 89 | Ok(fs) 90 | } 91 | 92 | pub fn user() -> Result { 93 | let user = match get_user_by_uid(get_current_uid()) { 94 | Some(u) => u, 95 | None => return Err("Could not resolve current user".into()), 96 | }; 97 | 98 | let group = match get_group_by_gid(user.primary_group_id()) { 99 | Some(g) => g, 100 | None => return Err("Could not resolve current group".into()), 101 | }; 102 | 103 | Ok(User { 104 | user: user.name().into(), 105 | uid: user.uid(), 106 | group: group.name().into(), 107 | gid: group.gid(), 108 | home_dir: user.home_dir().into(), 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /core/src/target/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use regex::Regex; 9 | use std::{fs, process, str}; 10 | use std::io::Read; 11 | 12 | #[derive(Eq, PartialEq)] 13 | pub enum LinuxFlavour { 14 | Centos, 15 | Debian, 16 | Fedora, 17 | Redhat, 18 | Ubuntu, 19 | Nixos, 20 | } 21 | 22 | pub fn fingerprint_os() -> Option { 23 | // @todo Cache this result 24 | 25 | // CentOS 26 | if let Ok(_) = fs::metadata("/etc/centos-release") { 27 | Some(LinuxFlavour::Centos) 28 | } 29 | // Ubuntu 30 | else if let Ok(_) = fs::metadata("/etc/lsb-release") { 31 | Some(LinuxFlavour::Ubuntu) 32 | } 33 | // Debian 34 | else if let Ok(_) = fs::metadata("/etc/debian_version") { 35 | Some(LinuxFlavour::Debian) 36 | } 37 | // Fedora 38 | else if let Ok(_) = fs::metadata("/etc/fedora-release") { 39 | Some(LinuxFlavour::Fedora) 40 | } 41 | // RedHat 42 | else if let Ok(_) = fs::metadata("/etc/redhat-release") { 43 | Some(LinuxFlavour::Redhat) 44 | } 45 | // NixOS 46 | else if let Ok(_) = fs::metadata("/etc/nixos/configuration.nix") { 47 | Some(LinuxFlavour::Nixos) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | pub fn cpu_vendor() -> Result { 54 | get_cpu_item("vendor_id") 55 | } 56 | 57 | pub fn cpu_brand_string() -> Result { 58 | get_cpu_item("model name") 59 | } 60 | 61 | pub fn cpu_cores() -> Result { 62 | Ok(get_cpu_item("cpu cores")? 63 | .parse::() 64 | .chain_err(|| ErrorKind::InvalidTelemetryKey { 65 | cmd: "/proc/cpuinfo", 66 | key: "cpu cores".into() 67 | })?) 68 | } 69 | 70 | fn get_cpu_item(item: &str) -> Result { 71 | // @todo Cache file content 72 | let mut fh = fs::File::open("/proc/cpuinfo").chain_err(|| ErrorKind::SystemFile("/proc/cpuinfo"))?; 73 | let mut cpuinfo = String::new(); 74 | fh.read_to_string(&mut cpuinfo).chain_err(|| ErrorKind::SystemFileOutput("/proc/cpuinfo"))?;; 75 | 76 | let pattern = format!(r"(?m)^{}\s+: (.+)$", item); 77 | let regex = Regex::new(&pattern).unwrap(); 78 | let capture = regex.captures(&cpuinfo); 79 | 80 | if let Some(cap) = capture { 81 | Ok(cap.get(1).unwrap().as_str().to_string()) 82 | } else { 83 | Err(ErrorKind::InvalidTelemetryKey { cmd: "/proc/cpuinfo", key: item.into() }.into()) 84 | } 85 | } 86 | 87 | pub fn memory() -> Result { 88 | let output = process::Command::new("free").arg("-b").output().chain_err(|| ErrorKind::SystemCommand("free"))?; 89 | let regex = Regex::new(r"(?m)^Mem:\s+([0-9]+)").chain_err(|| "could not create new Regex instance")?; 90 | let capture = regex.captures(str::from_utf8(&output.stdout).chain_err(|| ErrorKind::SystemCommandOutput("free"))?.trim()); 91 | 92 | if let Some(cap) = capture { 93 | Ok(cap.get(1).unwrap().as_str().parse::().chain_err(|| ErrorKind::SystemFileOutput("/etc/redhat-release"))?) 94 | } else { 95 | Err(ErrorKind::SystemCommandOutput("free").into()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/target/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | pub mod default; 8 | pub mod linux; 9 | pub mod redhat; 10 | pub mod unix; 11 | -------------------------------------------------------------------------------- /core/src/target/redhat.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use regex::Regex; 9 | use std::fs; 10 | use std::io::Read; 11 | 12 | pub fn version() -> Result<(String, u32, u32, u32)> { 13 | let mut fh = fs::File::open("/etc/redhat-release").chain_err(|| ErrorKind::SystemFile("/etc/redhat-release"))?; 14 | let mut fc = String::new(); 15 | fh.read_to_string(&mut fc).unwrap(); 16 | 17 | let regex = Regex::new(r"release ([0-9]+)(?:\.([0-9]+)(?:\.([0-9]+))?)?").unwrap(); 18 | if let Some(cap) = regex.captures(&fc) { 19 | let version_maj = cap.get(1).unwrap().as_str() 20 | .parse().chain_err(|| ErrorKind::SystemFileOutput("/etc/redhat-release"))?; 21 | let version_min = match cap.get(2) { 22 | Some(v) => v.as_str().parse().chain_err(|| ErrorKind::SystemFileOutput("/etc/redhat-release"))?, 23 | None => 0, 24 | }; 25 | let version_patch = match cap.get(3) { 26 | Some(v) => v.as_str().parse().chain_err(|| ErrorKind::SystemFileOutput("/etc/redhat-release"))?, 27 | None => 0, 28 | }; 29 | let version_str = format!("{}.{}.{}", version_maj, version_min, version_patch); 30 | Ok((version_str, version_maj, version_min, version_patch)) 31 | } else { 32 | Err(ErrorKind::SystemFileOutput("/etc/redhat-release").into()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/target/unix.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use regex::Regex; 9 | use std::{process, str}; 10 | // use std::path::Path; 11 | // use super::default; 12 | 13 | // pub fn file_get_owner>(path: P) -> Result { 14 | // Ok(FileOwner { 15 | // user_name: default::file_stat(path.as_ref(), vec!["-f", "%Su"])?, 16 | // user_uid: default::file_stat(path.as_ref(), vec!["-f", "%u"])?.parse::().unwrap(), 17 | // group_name: default::file_stat(path.as_ref(), vec!["-f", "%Sg"])?, 18 | // group_gid: default::file_stat(path.as_ref(), vec!["-f", "%g"])?.parse::().unwrap() 19 | // }) 20 | // } 21 | 22 | // pub fn file_get_mode>(path: P) -> Result { 23 | // Ok(default::file_stat(path, vec!["-f", "%Lp"])?.parse::().unwrap()) 24 | // } 25 | 26 | pub fn version() -> Result<(String, u32, u32)> { 27 | let output = process::Command::new("uname") 28 | .arg("-r") 29 | .output() 30 | .chain_err(|| ErrorKind::SystemCommand("uname"))?; 31 | let version_str = str::from_utf8(&output.stdout).unwrap().trim(); 32 | let regex = Regex::new(r"([0-9]+)\.([0-9]+)-[A-Z]+").chain_err(|| "could not create new Regex instance")?; 33 | let errstr = format!("Expected OS version format `u32.u32`, got: '{}'", version_str); 34 | if let Some(cap) = regex.captures(version_str) { 35 | let version_maj = cap.get(1).unwrap().as_str().parse().chain_err(|| ErrorKind::SystemCommandOutput("uname"))?; 36 | let version_min = cap.get(2).unwrap().as_str().parse().chain_err(|| ErrorKind::SystemCommandOutput("uname"))?; 37 | Ok((version_str.into(), version_maj, version_min)) 38 | } else { 39 | Err(errstr.into()) 40 | } 41 | } 42 | 43 | pub fn get_sysctl_item(item: &str) -> Result { 44 | // @todo Cache output of sysctl 45 | let sysctl_out = process::Command::new("sysctl") 46 | .arg("-a") 47 | .output() 48 | .chain_err(|| ErrorKind::SystemCommand("sysctl"))?; 49 | let sysctl = String::from_utf8(sysctl_out.stdout).chain_err(|| ErrorKind::SystemCommandOutput("sysctl"))?; 50 | 51 | let exp = format!("{}: (.+)", item); 52 | let regex = Regex::new(&exp).chain_err(|| "could not create new Regex instance")?; 53 | 54 | if let Some(cap) = regex.captures(&sysctl) { 55 | Ok(cap.get(1).unwrap().as_str().into()) 56 | } else { 57 | Err(ErrorKind::InvalidTelemetryKey { cmd: "sysctl", key: item.into() }.into()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/telemetry/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! System generated data about your host. 8 | //! 9 | //! Telemetry is retrieved automatically when you create a new `Host`, which is 10 | //! nice of it. Call [`Host.telemetry()`](../host/trait.Host.html#tymethod.telemetry) 11 | //! to access it. 12 | 13 | mod providers; 14 | #[doc(hidden)] pub mod serializable; 15 | 16 | use errors::*; 17 | use futures::Future; 18 | use host::Host; 19 | use pnet::datalink::NetworkInterface; 20 | use remote::{Request, Response}; 21 | #[doc(hidden)] pub use self::providers::factory; 22 | use std::path::PathBuf; 23 | 24 | /// Top level structure that contains static information about a `Host`. 25 | #[derive(Debug)] 26 | pub struct Telemetry { 27 | /// Information on the CPU 28 | pub cpu: Cpu, 29 | /// Information on the filesystem 30 | pub fs: Vec, 31 | /// Host's FQDN 32 | pub hostname: String, 33 | /// Amount of RAM, in bytes 34 | pub memory: u64, 35 | /// Information on network interfaces 36 | pub net: Vec, 37 | /// Information about the operating system 38 | pub os: Os, 39 | /// Information on the current user 40 | pub user: User, 41 | } 42 | 43 | /// Information about the `Host`s CPU. 44 | #[derive(Debug, Serialize, Deserialize)] 45 | pub struct Cpu { 46 | /// Processor vendor, e.g. "GenuineIntel" 47 | pub vendor: String, 48 | /// Full description of the processor 49 | pub brand_string: String, 50 | /// Number of cores in the processor 51 | pub cores: u32, 52 | } 53 | 54 | /// Information about a specific filesystem mount. 55 | #[derive(Debug, Serialize, Deserialize)] 56 | pub struct FsMount { 57 | /// The device path, e.g. /dev/sd0s1 58 | pub filesystem: String, 59 | /// Path to where the device is mounted, e.g. /boot 60 | pub mountpoint: String, 61 | /// Capacity of device in Kb 62 | pub size: u64, 63 | /// Amount used in Kb 64 | pub used: u64, 65 | /// Remaining capacity available in Kb 66 | pub available: u64, 67 | /// Percentage used as a decimal 68 | pub capacity: f32, 69 | } 70 | 71 | /// Information about the `Host`s OS. 72 | #[derive(Debug, Serialize, Deserialize)] 73 | pub struct Os { 74 | /// OS architecture, e.g. "x86_64" 75 | pub arch: String, 76 | /// OS family 77 | pub family: OsFamily, 78 | /// OS name 79 | pub platform: OsPlatform, 80 | /// Full version string, e.g. "10.13" 81 | pub version_str: String, 82 | /// Major version number, e.g. "10" 83 | pub version_maj: u32, 84 | /// Minor version number, e.g. "13" 85 | pub version_min: u32, 86 | /// Patch version number, e.g. "0" 87 | pub version_patch: u32, 88 | } 89 | 90 | /// Operating system family 91 | #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] 92 | pub enum OsFamily { 93 | Bsd, 94 | Darwin, 95 | Linux(LinuxDistro), 96 | } 97 | 98 | /// Operating system name 99 | #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] 100 | pub enum OsPlatform { 101 | Centos, 102 | Debian, 103 | Fedora, 104 | Freebsd, 105 | Macos, 106 | Nixos, 107 | Ubuntu, 108 | } 109 | 110 | /// Linux distribution name 111 | #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] 112 | pub enum LinuxDistro { 113 | Debian, 114 | RHEL, 115 | Standalone, 116 | } 117 | 118 | /// Information on the current user 119 | #[derive(Debug, Serialize, Deserialize)] 120 | pub struct User { 121 | pub user: String, 122 | pub uid: u32, 123 | pub group: String, 124 | pub gid: u32, 125 | pub home_dir: PathBuf, 126 | } 127 | 128 | impl Telemetry { 129 | pub fn load(host: &H) -> Box> { 130 | Box::new(host.request(Request::TelemetryLoad) 131 | .chain_err(|| ErrorKind::Request { endpoint: "Telemetry", func: "load" }) 132 | .map(|msg| match msg.into_inner() { 133 | Response::TelemetryLoad(t) => Telemetry::from(t), 134 | _ => unreachable!(), 135 | })) 136 | } 137 | } 138 | 139 | impl User { 140 | // Whether this user is root, which is calculated as `uid == 0`. 141 | pub fn is_root(&self) -> bool { 142 | self.uid == 0 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/centos.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use remote::{ExecutableResult, Response, ResponseResult}; 11 | use std::env; 12 | use super::TelemetryProvider; 13 | use target::{default, linux, redhat}; 14 | use target::linux::LinuxFlavour; 15 | use telemetry::{Cpu, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 16 | use tokio_proto::streaming::Message; 17 | 18 | pub struct Centos; 19 | 20 | impl TelemetryProvider for Centos { 21 | fn available() -> bool { 22 | cfg!(target_os="linux") && linux::fingerprint_os() == Some(LinuxFlavour::Centos) 23 | } 24 | 25 | fn load(&self) -> ExecutableResult { 26 | Box::new(future::lazy(|| { 27 | let t = match do_load() { 28 | Ok(t) => t, 29 | Err(e) => return future::err(e), 30 | }; 31 | 32 | future::ok(Message::WithoutBody( 33 | ResponseResult::Ok( 34 | Response::TelemetryLoad(t.into())))) 35 | })) 36 | } 37 | } 38 | 39 | fn do_load() -> Result { 40 | let (version_str, version_maj, version_min, version_patch) = redhat::version()?; 41 | 42 | Ok(Telemetry { 43 | cpu: Cpu { 44 | vendor: linux::cpu_vendor()?, 45 | brand_string: linux::cpu_brand_string()?, 46 | cores: linux::cpu_cores()?, 47 | }, 48 | fs: default::fs().chain_err(|| "could not resolve telemetry data")?, 49 | hostname: default::hostname()?, 50 | memory: linux::memory().chain_err(|| "could not resolve telemetry data")?, 51 | net: interfaces(), 52 | os: Os { 53 | arch: env::consts::ARCH.into(), 54 | family: OsFamily::Linux(LinuxDistro::RHEL), 55 | platform: OsPlatform::Centos, 56 | version_str: version_str, 57 | version_maj: version_maj, 58 | version_min: version_min, 59 | version_patch: version_patch, 60 | }, 61 | user: default::user()?, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/debian.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use remote::{ExecutableResult, Response, ResponseResult}; 11 | use std::{env, process, str}; 12 | use super::TelemetryProvider; 13 | use target::{default, linux}; 14 | use target::linux::LinuxFlavour; 15 | use telemetry::{Cpu, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 16 | use tokio_proto::streaming::Message; 17 | 18 | pub struct Debian; 19 | 20 | impl TelemetryProvider for Debian { 21 | fn available() -> bool { 22 | cfg!(target_os="linux") && linux::fingerprint_os() == Some(LinuxFlavour::Debian) 23 | } 24 | 25 | fn load(&self) -> ExecutableResult { 26 | Box::new(future::lazy(|| { 27 | let t = match do_load() { 28 | Ok(t) => t, 29 | Err(e) => return future::err(e), 30 | }; 31 | 32 | future::ok(Message::WithoutBody( 33 | ResponseResult::Ok( 34 | Response::TelemetryLoad(t.into())))) 35 | })) 36 | } 37 | } 38 | 39 | fn do_load() -> Result { 40 | let (version_str, version_maj, version_min) = version()?; 41 | 42 | Ok(Telemetry { 43 | cpu: Cpu { 44 | vendor: linux::cpu_vendor()?, 45 | brand_string: linux::cpu_brand_string()?, 46 | cores: linux::cpu_cores()?, 47 | }, 48 | fs: default::fs().chain_err(|| "could not resolve telemetry data")?, 49 | hostname: default::hostname()?, 50 | memory: linux::memory().chain_err(|| "could not resolve telemetry data")?, 51 | net: interfaces(), 52 | os: Os { 53 | arch: env::consts::ARCH.into(), 54 | family: OsFamily::Linux(LinuxDistro::Debian), 55 | platform: OsPlatform::Debian, 56 | version_str: version_str, 57 | version_maj: version_maj, 58 | version_min: version_min, 59 | version_patch: 0, 60 | }, 61 | user: default::user()?, 62 | }) 63 | } 64 | 65 | fn version() -> Result<(String, u32, u32)> { 66 | let out = process::Command::new("lsb_release") 67 | .arg("-sr") 68 | .output() 69 | .chain_err(|| ErrorKind::SystemCommand("lsb_release"))?; 70 | let version_str = str::from_utf8(&out.stdout) 71 | .chain_err(|| ErrorKind::SystemCommandOutput("lsb_release"))? 72 | .trim() 73 | .to_owned(); 74 | let (maj, min) = { 75 | let mut parts = version_str.split('.'); 76 | let errstr = format!("Expected OS version format `u32.u32`, got: '{}'", version_str); 77 | ( 78 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))?, 79 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))? 80 | ) 81 | }; 82 | Ok((version_str, maj, min)) 83 | } 84 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/fedora.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use remote::{ExecutableResult, Response, ResponseResult}; 11 | use std::env; 12 | use super::TelemetryProvider; 13 | use target::{default, linux, redhat}; 14 | use target::linux::LinuxFlavour; 15 | use telemetry::{Cpu, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 16 | use tokio_proto::streaming::Message; 17 | 18 | pub struct Fedora; 19 | 20 | impl TelemetryProvider for Fedora { 21 | fn available() -> bool { 22 | cfg!(target_os="linux") && linux::fingerprint_os() == Some(LinuxFlavour::Fedora) 23 | } 24 | 25 | fn load(&self) -> ExecutableResult { 26 | Box::new(future::lazy(|| { 27 | let t = match do_load() { 28 | Ok(t) => t, 29 | Err(e) => return future::err(e), 30 | }; 31 | 32 | future::ok(Message::WithoutBody( 33 | ResponseResult::Ok( 34 | Response::TelemetryLoad(t.into())))) 35 | })) 36 | } 37 | } 38 | 39 | fn do_load() -> Result { 40 | let (version_str, version_maj, version_min, version_patch) = redhat::version()?; 41 | 42 | Ok(Telemetry { 43 | cpu: Cpu { 44 | vendor: linux::cpu_vendor()?, 45 | brand_string: linux::cpu_brand_string()?, 46 | cores: linux::cpu_cores()?, 47 | }, 48 | fs: default::fs().chain_err(|| "could not resolve telemetry data")?, 49 | hostname: default::hostname()?, 50 | memory: linux::memory().chain_err(|| "could not resolve telemetry data")?, 51 | net: interfaces(), 52 | os: Os { 53 | arch: env::consts::ARCH.into(), 54 | family: OsFamily::Linux(LinuxDistro::RHEL), 55 | platform: OsPlatform::Fedora, 56 | version_str: version_str, 57 | version_maj: version_maj, 58 | version_min: version_min, 59 | version_patch: version_patch, 60 | }, 61 | user: default::user()?, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/freebsd.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use regex::Regex; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::{env, fs}; 13 | use std::io::Read; 14 | use super::TelemetryProvider; 15 | use target::{default, unix}; 16 | use telemetry::{Cpu, Os, OsFamily, OsPlatform, Telemetry}; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Freebsd; 20 | 21 | impl TelemetryProvider for Freebsd { 22 | fn available() -> bool { 23 | cfg!(target_os="freebsd") 24 | } 25 | 26 | fn load(&self) -> ExecutableResult { 27 | Box::new(future::lazy(|| { 28 | let t = match do_load() { 29 | Ok(t) => t, 30 | Err(e) => return future::err(e), 31 | }; 32 | 33 | future::ok(Message::WithoutBody( 34 | ResponseResult::Ok( 35 | Response::TelemetryLoad(t.into())))) 36 | })) 37 | } 38 | } 39 | 40 | fn do_load() -> Result { 41 | let (version_str, version_maj, version_min) = unix::version()?; 42 | 43 | Ok(Telemetry { 44 | cpu: Cpu { 45 | vendor: telemetry_cpu_vendor()?, 46 | brand_string: unix::get_sysctl_item("hw\\.model")?, 47 | cores: unix::get_sysctl_item("hw\\.ncpu") 48 | .chain_err(|| "could not resolve telemetry data")? 49 | .parse::() 50 | .chain_err(|| "could not resolve telemetry data")?, 51 | }, 52 | fs: default::fs()?, 53 | hostname: default::hostname()?, 54 | memory: unix::get_sysctl_item("hw\\.physmem") 55 | .chain_err(|| "could not resolve telemetry data")? 56 | .parse::() 57 | .chain_err(|| "could not resolve telemetry data")?, 58 | net: interfaces(), 59 | os: Os { 60 | arch: env::consts::ARCH.into(), 61 | family: OsFamily::Bsd, 62 | platform: OsPlatform::Freebsd, 63 | version_str: version_str, 64 | version_maj: version_maj, 65 | version_min: version_min, 66 | version_patch: 0 67 | }, 68 | user: default::user()?, 69 | }) 70 | } 71 | 72 | fn telemetry_cpu_vendor() -> Result { 73 | let mut fh = fs::File::open("/var/run/dmesg.boot") 74 | .chain_err(|| ErrorKind::SystemFile("/var/run/dmesg.boot"))?; 75 | let mut fc = String::new(); 76 | fh.read_to_string(&mut fc).chain_err(|| ErrorKind::SystemFileOutput("/var/run/dmesg.boot"))?; 77 | 78 | let regex = Regex::new(r#"(?m)^CPU:.+$\n\s+Origin="([A-Za-z]+)""#).unwrap(); 79 | if let Some(cap) = regex.captures(&fc) { 80 | Ok(cap.get(1).unwrap().as_str().into()) 81 | } else { 82 | Err(ErrorKind::SystemFileOutput("/var/run/dmesg.boot").into()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/macos.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use remote::{ExecutableResult, Response, ResponseResult}; 11 | use std::{env, process, str}; 12 | use super::TelemetryProvider; 13 | use target::{default, unix}; 14 | use telemetry::{Cpu, Os, OsFamily, OsPlatform, Telemetry}; 15 | use tokio_proto::streaming::Message; 16 | 17 | pub struct Macos; 18 | 19 | impl TelemetryProvider for Macos { 20 | fn available() -> bool { 21 | cfg!(target_os="macos") 22 | } 23 | 24 | fn load(&self) -> ExecutableResult { 25 | Box::new(future::lazy(|| { 26 | let t = match do_load() { 27 | Ok(t) => t, 28 | Err(e) => return future::err(e), 29 | }; 30 | 31 | future::ok(Message::WithoutBody( 32 | ResponseResult::Ok( 33 | Response::TelemetryLoad(t.into())))) 34 | })) 35 | } 36 | } 37 | 38 | fn do_load() -> Result { 39 | let (version_str, version_maj, version_min, version_patch) = version()?; 40 | 41 | Ok(Telemetry { 42 | cpu: Cpu { 43 | vendor: unix::get_sysctl_item("machdep\\.cpu\\.vendor")?, 44 | brand_string: unix::get_sysctl_item("machdep\\.cpu\\.brand_string")?, 45 | cores: unix::get_sysctl_item("hw\\.physicalcpu") 46 | .chain_err(|| "could not resolve telemetry data")? 47 | .parse::() 48 | .chain_err(|| "could not resolve telemetry data")? 49 | }, 50 | fs: default::parse_fs(&[ 51 | default::FsFieldOrder::Filesystem, 52 | default::FsFieldOrder::Size, 53 | default::FsFieldOrder::Used, 54 | default::FsFieldOrder::Available, 55 | default::FsFieldOrder::Capacity, 56 | default::FsFieldOrder::Blank, 57 | default::FsFieldOrder::Blank, 58 | default::FsFieldOrder::Blank, 59 | default::FsFieldOrder::Mount, 60 | ])?, 61 | hostname: default::hostname()?, 62 | memory: unix::get_sysctl_item("hw\\.memsize") 63 | .chain_err(|| "could not resolve telemetry data")? 64 | .parse::() 65 | .chain_err(|| "could not resolve telemetry data")?, 66 | net: interfaces(), 67 | os: Os { 68 | arch: env::consts::ARCH.into(), 69 | family: OsFamily::Darwin, 70 | platform: OsPlatform::Macos, 71 | version_str: version_str, 72 | version_maj: version_maj, 73 | version_min: version_min, 74 | version_patch: version_patch 75 | }, 76 | user: default::user()?, 77 | }) 78 | } 79 | 80 | fn version() -> Result<(String, u32, u32, u32)> { 81 | let out = process::Command::new("sw_vers") 82 | .arg("-productVersion") 83 | .output() 84 | .chain_err(|| ErrorKind::SystemCommand("sw_vers"))?; 85 | let version_str = str::from_utf8(&out.stdout) 86 | .chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))? 87 | .trim() 88 | .to_owned(); 89 | let (maj, min, patch) = { 90 | let mut parts = version_str.split('.'); 91 | let errstr = format!("Expected OS version format `u32.u32[.u32]`, got: '{}'", version_str); 92 | ( 93 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))?, 94 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))?, 95 | parts.next().unwrap_or("0").parse().chain_err(|| ErrorKind::SystemCommandOutput("sw_vers"))? 96 | ) 97 | }; 98 | Ok((version_str, maj, min, patch)) 99 | } 100 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | //! OS abstractions for `Telemetry`. 8 | 9 | mod centos; 10 | mod debian; 11 | mod fedora; 12 | mod freebsd; 13 | mod macos; 14 | mod nixos; 15 | mod ubuntu; 16 | 17 | pub use self::centos::Centos; 18 | pub use self::debian::Debian; 19 | pub use self::fedora::Fedora; 20 | pub use self::freebsd::Freebsd; 21 | pub use self::macos::Macos; 22 | pub use self::nixos::Nixos; 23 | pub use self::ubuntu::Ubuntu; 24 | 25 | use errors::*; 26 | use remote::ExecutableResult; 27 | 28 | pub trait TelemetryProvider { 29 | fn available() -> bool where Self: Sized; 30 | fn load(&self) -> ExecutableResult; 31 | } 32 | 33 | #[doc(hidden)] 34 | pub fn factory() -> Result> { 35 | if Centos::available() { 36 | Ok(Box::new(Centos)) 37 | } 38 | else if Debian::available() { 39 | Ok(Box::new(Debian)) 40 | } 41 | else if Fedora::available() { 42 | Ok(Box::new(Fedora)) 43 | } 44 | else if Freebsd::available() { 45 | Ok(Box::new(Freebsd)) 46 | } 47 | else if Macos::available() { 48 | Ok(Box::new(Macos)) 49 | } 50 | else if Nixos::available() { 51 | Ok(Box::new(Nixos)) 52 | } 53 | else if Ubuntu::available() { 54 | Ok(Box::new(Ubuntu)) 55 | } else { 56 | Err(ErrorKind::ProviderUnavailable("Telemetry").into()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/nixos.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use remote::{ExecutableResult, Response, ResponseResult}; 11 | use std::{env, process, str}; 12 | use super::TelemetryProvider; 13 | use target::{default, linux}; 14 | use target::linux::LinuxFlavour; 15 | use telemetry::{Cpu, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 16 | use tokio_proto::streaming::Message; 17 | 18 | pub struct Nixos; 19 | 20 | impl TelemetryProvider for Nixos { 21 | fn available() -> bool { 22 | cfg!(target_os="linux") && linux::fingerprint_os() == Some(LinuxFlavour::Nixos) 23 | } 24 | 25 | fn load(&self) -> ExecutableResult { 26 | Box::new(future::lazy(|| { 27 | let t = match do_load() { 28 | Ok(t) => t, 29 | Err(e) => return future::err(e), 30 | }; 31 | 32 | future::ok(Message::WithoutBody( 33 | ResponseResult::Ok( 34 | Response::TelemetryLoad(t.into())))) 35 | })) 36 | } 37 | } 38 | 39 | fn do_load() -> Result { 40 | let (version_str, version_maj, version_min, version_patch) = version()?; 41 | 42 | Ok(Telemetry { 43 | cpu: Cpu { 44 | vendor: linux::cpu_vendor()?, 45 | brand_string: linux::cpu_brand_string()?, 46 | cores: linux::cpu_cores()?, 47 | }, 48 | fs: default::fs().chain_err(|| "could not resolve telemetry data")?, 49 | hostname: default::hostname()?, 50 | memory: linux::memory().chain_err(|| "could not resolve telemetry data")?, 51 | net: interfaces(), 52 | os: Os { 53 | arch: env::consts::ARCH.into(), 54 | family: OsFamily::Linux(LinuxDistro::Standalone), 55 | platform: OsPlatform::Nixos, 56 | version_str: version_str, 57 | version_maj: version_maj, 58 | version_min: version_min, 59 | version_patch: version_patch 60 | }, 61 | user: default::user()?, 62 | }) 63 | } 64 | 65 | fn version() -> Result<(String, u32, u32, u32)> { 66 | let out = process::Command::new("nixos-version") 67 | .output() 68 | .chain_err(|| ErrorKind::SystemCommand("nixos-version"))?; 69 | let version_str = str::from_utf8(&out.stdout) 70 | .chain_err(|| ErrorKind::SystemCommandOutput("nixos-version"))? 71 | .trim() 72 | .to_owned(); 73 | let (maj, min, patch) = { 74 | let mut parts = version_str.split('.'); 75 | let errstr = format!("Expected OS version format `u32.u32.u32.hash (codename)`, got: '{}'", version_str); 76 | ( 77 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("nixos-version"))?, 78 | parts.next().ok_or(&*errstr)?.parse().chain_err(|| ErrorKind::SystemCommandOutput("nixos-version"))?, 79 | parts.next().unwrap_or("0").parse().chain_err(|| ErrorKind::SystemCommandOutput("nixos-version"))? 80 | ) 81 | }; 82 | Ok((version_str, maj, min, patch)) 83 | } 84 | -------------------------------------------------------------------------------- /core/src/telemetry/providers/ubuntu.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | use errors::*; 8 | use futures::future; 9 | use pnet::datalink::interfaces; 10 | use regex::Regex; 11 | use remote::{ExecutableResult, Response, ResponseResult}; 12 | use std::{env, process, str}; 13 | use super::TelemetryProvider; 14 | use target::{default, linux}; 15 | use target::linux::LinuxFlavour; 16 | use telemetry::{Cpu, LinuxDistro, Os, OsFamily, OsPlatform, Telemetry}; 17 | use tokio_proto::streaming::Message; 18 | 19 | pub struct Ubuntu; 20 | 21 | impl TelemetryProvider for Ubuntu { 22 | fn available() -> bool { 23 | cfg!(target_os="linux") && linux::fingerprint_os() == Some(LinuxFlavour::Ubuntu) 24 | } 25 | 26 | fn load(&self) -> ExecutableResult { 27 | Box::new(future::lazy(|| { 28 | let t = match do_load() { 29 | Ok(t) => t, 30 | Err(e) => return future::err(e), 31 | }; 32 | 33 | future::ok(Message::WithoutBody( 34 | ResponseResult::Ok( 35 | Response::TelemetryLoad(t.into())))) 36 | })) 37 | } 38 | } 39 | 40 | fn do_load() -> Result { 41 | let (version_str, version_maj, version_min, version_patch) = version()?; 42 | 43 | Ok(Telemetry { 44 | cpu: Cpu { 45 | vendor: linux::cpu_vendor()?, 46 | brand_string: linux::cpu_brand_string()?, 47 | cores: linux::cpu_cores()?, 48 | }, 49 | fs: default::fs().chain_err(|| "could not resolve telemetry data")?, 50 | hostname: default::hostname()?, 51 | memory: linux::memory().chain_err(|| "could not resolve telemetry data")?, 52 | net: interfaces(), 53 | os: Os { 54 | arch: env::consts::ARCH.into(), 55 | family: OsFamily::Linux(LinuxDistro::Debian), 56 | platform: OsPlatform::Ubuntu, 57 | version_str: version_str, 58 | version_maj: version_maj, 59 | version_min: version_min, 60 | version_patch: version_patch, 61 | }, 62 | user: default::user()?, 63 | }) 64 | } 65 | 66 | fn version() -> Result<(String, u32, u32, u32)> { 67 | let out = process::Command::new("lsb_release").arg("-sd").output()?; 68 | let desc = str::from_utf8(&out.stdout) 69 | .chain_err(|| ErrorKind::SystemCommand("Ubuntu-version"))?; 70 | 71 | let regex = Regex::new(r"([0-9]+)\.([0-9]+)\.([0-9]+)( LTS)?").unwrap(); 72 | if let Some(cap) = regex.captures(&desc) { 73 | let version_maj = cap.get(1).unwrap().as_str().parse().chain_err(|| ErrorKind::SystemCommandOutput("lsb_release -sd"))?; 74 | let version_min = cap.get(2).unwrap().as_str().parse().chain_err(|| ErrorKind::SystemCommandOutput("lsb_release -sd"))?; 75 | let version_patch = cap.get(3).unwrap().as_str().parse().chain_err(|| ErrorKind::SystemCommandOutput("lsb_release -sd"))?; 76 | let mut version_str = format!("{}.{}.{}", version_maj, version_min, version_patch); 77 | if cap.get(4).is_some() { 78 | version_str.push_str(" LTS"); 79 | } 80 | Ok((version_str, version_maj, version_min, version_patch)) 81 | } else { 82 | Err(ErrorKind::SystemCommandOutput("lsb_release -sd").into()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/telemetry/serializable.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | // @todo If Rust ever relaxes its orphan rules, we'll be able to 8 | // implement Serialize/Deserialize on 3rd party structures. 9 | // See: https://github.com/rust-lang/rfcs/issues/1856 10 | 11 | use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; 12 | use pnet::datalink::NetworkInterface; 13 | use pnet::util::MacAddr; 14 | use std::convert::From; 15 | use std::net::{Ipv4Addr, Ipv6Addr}; 16 | use std::str::FromStr; 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct Telemetry { 20 | pub cpu: super::Cpu, 21 | pub fs: Vec, 22 | pub hostname: String, 23 | pub memory: u64, 24 | pub net: Vec, 25 | pub os: super::Os, 26 | pub user: super::User, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | pub struct Netif { 31 | pub name: String, 32 | pub index: u32, 33 | pub mac: Option, 34 | pub ips: Vec, 35 | pub flags: u32, 36 | } 37 | 38 | #[derive(Serialize, Deserialize)] 39 | pub enum IpNet { 40 | V4(Ipv4Net), 41 | V6(Ipv6Net), 42 | } 43 | 44 | #[derive(Serialize, Deserialize)] 45 | pub struct Ipv4Net { 46 | ip: Ipv4Addr, 47 | prefix: u8, 48 | } 49 | 50 | #[derive(Serialize, Deserialize)] 51 | pub struct Ipv6Net { 52 | ip: Ipv6Addr, 53 | prefix: u8, 54 | } 55 | 56 | impl From for Telemetry { 57 | fn from(t: super::Telemetry) -> Telemetry { 58 | let net = t.net.into_iter().map(|iface| Netif { 59 | name: iface.name, 60 | index: iface.index, 61 | mac: iface.mac.map(|addr| addr.to_string()), 62 | ips: iface.ips.into_iter().map(|net| match net { 63 | IpNetwork::V4(n) => IpNet::V4(Ipv4Net { 64 | ip: n.ip(), 65 | prefix: n.prefix(), 66 | }), 67 | IpNetwork::V6(n) => IpNet::V6(Ipv6Net { 68 | ip: n.ip(), 69 | prefix: n.prefix(), 70 | }) 71 | }).collect(), 72 | flags: iface.flags, 73 | }).collect(); 74 | 75 | Telemetry { 76 | cpu: t.cpu, 77 | fs: t.fs, 78 | hostname: t.hostname, 79 | memory: t.memory, 80 | net: net, 81 | os: t.os, 82 | user: t.user, 83 | } 84 | } 85 | } 86 | 87 | impl From for super::Telemetry { 88 | fn from(t: Telemetry) -> super::Telemetry { 89 | let net = t.net.into_iter().map(|iface| NetworkInterface { 90 | name: iface.name, 91 | index: iface.index, 92 | mac: iface.mac.map(|addr| MacAddr::from_str(&addr).unwrap()), 93 | ips: iface.ips.into_iter().map(|net| match net { 94 | IpNet::V4(n) => IpNetwork::V4(Ipv4Network::new(n.ip, n.prefix).unwrap()), 95 | IpNet::V6(n) => IpNetwork::V6(Ipv6Network::new(n.ip, n.prefix).unwrap()), 96 | }).collect(), 97 | flags: iface.flags, 98 | }).collect(); 99 | 100 | super::Telemetry { 101 | cpu: t.cpu, 102 | fs: t.fs, 103 | hostname: t.hostname, 104 | memory: t.memory, 105 | net: net, 106 | os: t.os, 107 | user: t.user, 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /proj/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intecture_proj" 3 | version = "0.0.1" 4 | authors = [ "Pete Hayes " ] 5 | license = "MPL-2.0" 6 | description = "Helpers and boilerplate for building Intecture projects" 7 | homepage = "https://intecture.io" 8 | repository = "https://github.com/intecture/api" 9 | documentation = "https://intecture.io/rust/inapi/" 10 | keywords = ["intecture", "project", "proj"] 11 | categories = ["servers"] 12 | readme = "README.md" 13 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] 14 | 15 | [badges] 16 | travis-ci = { repository = "intecture/api" } 17 | 18 | [dependencies] 19 | error-chain = "0.11" 20 | intecture_api = { version = "0.4.0", path = "../core" } 21 | -------------------------------------------------------------------------------- /proj/README.md: -------------------------------------------------------------------------------- 1 | # Project 2 | 3 | The project API contains helpers and boilerplate for creating Intecture projects, as well as reexporting the core API. Usually you'll use this API, however for applications that do not require a formal structure, the core API ([core/](../core/)) may be used instead. 4 | -------------------------------------------------------------------------------- /proj/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2017 Intecture Developers. 2 | // 3 | // Licensed under the Mozilla Public License 2.0 . This file may not be copied, 5 | // modified, or distributed except according to those terms. 6 | 7 | extern crate intecture_api; 8 | 9 | pub mod prelude { 10 | pub use intecture_api::prelude::*; 11 | } 12 | 13 | pub use intecture_api::*; 14 | --------------------------------------------------------------------------------