├── .editorconfig ├── .gitattributes ├── .github ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── appveyor.yml ├── bin ├── idoitcli ├── idoitcli-dbg └── idoitcli.php ├── composer.json ├── config ├── applications.json ├── dcim.json ├── default.json ├── location.json ├── networks.json ├── office.json ├── schema.json └── template.json ├── idoit.bash-completion ├── phpunit.xml └── src ├── Command ├── Cache.php ├── Call.php ├── Categories.php ├── Command.php ├── Create.php ├── Dummy.php ├── FixIP.php ├── Init.php ├── Log.php ├── Logs.php ├── Network.php ├── NextIP.php ├── Rack.php ├── Random.php ├── Read.php ├── Save.php ├── Search.php ├── Show.php ├── Status.php └── Types.php └── Service ├── Cache.php ├── HandleAttribute.php ├── IdoitAPI.php ├── IdoitAPIFactory.php ├── IdoitStatus.php ├── PrintData.php ├── Service.php └── Validate.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.yml] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.php diff=php 3 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### Expected behavior 9 | 10 | 11 | 12 | ### Actual behavior 13 | 14 | 15 | 16 | ### Steps to reproduce the behavior 17 | 18 | 19 | 20 | ### Environment 21 | 22 | | Question | Answer | 23 | | ------------------------- | ----------------- | 24 | | bheisig/idoitapi version | x.y.z | 25 | | i-doit version | x.y.z open/pro | 26 | | i-doit API add-on version | x.y.z | 27 | 28 | ### Logs and other useful output 29 | 30 | 41 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | Short description 6 | 7 | Long description 8 | 9 | ### Added 10 | 11 | - First 12 | - Second 13 | - And so on… 14 | 15 | ### Changed 16 | 17 | - First 18 | - Second 19 | - And so on… 20 | 21 | ### Fixed 22 | 23 | - First 24 | - Second 25 | - And so on… 26 | 27 | ### Removed 28 | 29 | - First 30 | - Second 31 | - And so on… 32 | 33 | ### Confirmation 34 | 35 | By sending this pull request I accept the following conditions: 36 | 37 | - [ ] I have read the code of conduct and accept it. 38 | - [ ] I have read the contributing guidelines and accept them. 39 | - [ ] I accept that my contribution will be licensed under the AGPLv3. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | *.tar.gz 4 | *.deb 5 | composer.lock 6 | README 7 | /idoit 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | 4 | language: php 5 | 6 | php: 7 | - 7.1 8 | - 7.2 9 | - 7.3 10 | - 7.4snapshot 11 | 12 | install: 13 | - travis_retry composer self-update 14 | - travis_retry composer install --prefer-source --no-interaction 15 | 16 | script: 17 | - composer ci 18 | 19 | notifications: 20 | email: 21 | on_success: never 22 | on_failure: always 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased][] 9 | 10 | _tbd_ 11 | 12 | ## [0.9][] – 2020-01-08 13 | 14 | ### Added 15 | 16 | - `network`: Add new command to print list of IPv4 addresses 17 | - `rack`: Add new command to visualize hardware racks 18 | 19 | ### Changed 20 | 21 | - `save`: Handle more attributes as text fields 22 | 23 | ### Fixed 24 | 25 | - `save`: Do not ignore capacity (float) incl. unit (b, kb, mb, gb, tb) 26 | - `read`: Print object types when no arguments are given 27 | 28 | ## [0.8][] – 2019-07-31 29 | 30 | ☀️☀️☀️ Happy summer time ☀️☀️☀️ 31 | 32 | ## Changed 33 | 34 | - Require PHP >= 7.1 (PHP 7.0 is end-of-life); recommend PHP 7.3 35 | - i-doit 1.13 or higher is required 36 | - i-doit API add-on 1.10.3 is required 37 | - Require PHP extension `zlib` instead of `bz2` 38 | 39 | ## Fixed 40 | 41 | - Allow API calls without credentials 42 | - `save`/`show`: Prevent PHP fatal error when encoding empty dialog+ attribute 43 | - `show`: Prevent any unwanted user interaction (`--yes`) if object not found or selection ambiguous 44 | - Make CLI option `-y`/`--yes` work after it was completely ignored 45 | - `read`: Convert values to strings before printing them which prevents PHP fatal errors 46 | 47 | ## [0.7][] – 2018-12-18 48 | 49 | This release is a great step forward with new features and tons of improvements. Happy holidays! 50 | 51 | ### Added 52 | 53 | - `save`: Create/update CMDB objects and their category entries 54 | - `log`: Add entry to i-doit logbook 55 | - `logs`: Print entries from i-doit logbook 56 | - `cache`: Create cache files needed for faster processing 57 | - Add support for custom categories with user-defined attributes 58 | 59 | ### Changed 60 | 61 | - `init`: Just create configuration file and do not cache files anymore 62 | - `help`/`-h`/`--help`: Print more information about using each command 63 | - Ignore categories which have no proper attributes 64 | 65 | ### Fixed 66 | 67 | - `init`: Ask to enable proxy settings 68 | 69 | ## [0.6][] 70 | 71 | ### Added 72 | 73 | - Support for Windows operating systems 74 | - `random`: Create persons with random names, e-mail addresses and desks 75 | - `random`: Install applications on laptops with license keys 76 | 77 | ### Fixed 78 | 79 | - `init`: Validation error occurs even before initialization could start 80 | - `init`: Ask for hostname if HTTP proxy will be used 81 | 82 | ## [0.5][] – 2018-04-24 83 | 84 | **Important notes:** 85 | 86 | 1. PHP 5.6 support is dropped. You need PHP 7.0 or higher. 87 | 2. Re-load cache with `idoitcli init` 88 | 89 | ### Added 90 | 91 | - `create`: Create new object or category entry 92 | 93 | ### Changed 94 | 95 | - Renamed binary file to `idoitcli` 96 | - Separate CLI-related code from application code 97 | - Drop PHP 5.6 support and require PHP 7.0 or higher 98 | 99 | ### Fixed 100 | 101 | - `show`: Not all category entries are printed 102 | - `init`: Misleading cache file names for categories 103 | 104 | ## [0.4][] – 2017-09-21 105 | 106 | ### Added 107 | 108 | - `read`: Allow wildcards (`*`) in object titles 109 | - `categories`: Print a list of available categories 110 | - `types`: Print a list of available object types and group them 111 | - `fixip`: Assign IPs to subnets 112 | - Display error when user asks for specific category entries but category is not assigned to object type(s) 113 | 114 | ## [0.3][] – 2017-07-25 115 | 116 | ### Added 117 | 118 | - `show`: Show everything about an object 119 | - `read`: Read object by its identifier 120 | - `call`: Perform self-defined API requests 121 | - Find errors in additional configuration files 122 | 123 | ### Fixed 124 | 125 | - Show the right command description (`idoit help [command]` or `idoit [command] --help`) 126 | - Command `idoit read` was unable to fetch a list of objects or attributes for an object when configuration setting `limitBatchRequest` is disabled (`0`). 127 | 128 | ## [0.2][] – 2017-04-06 129 | 130 | ### Added 131 | 132 | - Find your data with new command `search` 133 | - List attributes and assigned categories with command `read` 134 | - Sort results with command `read` 135 | - Strip HTML code in results for command `read` 136 | - Limit batch requests (if configured) for command `read` with setting `limitBatchRequests` 137 | - Describe a lot of examples for command `read` in [documentation](README.md) und built-in help 138 | - Simple bash completion 139 | - Create racks and servers with categories "formfactor", "cpu", "model", and "location" with command `random` 140 | - Put servers into empty racks with command `random` 141 | - More examples in `docs/` for command `random` 142 | - Show message if cache is out-dated, see configuration setting `cacheLifetime` 143 | 144 | ### Fixed 145 | 146 | - Errors in built-in `help` 147 | - Removed out-dated command line options 148 | - Source code documentation 149 | 150 | ## 0.1 – 2017-02-06 151 | 152 | Initial release 153 | 154 | [Unreleased]: https://github.com/bheisig/i-doit-cli/compare/0.9...HEAD 155 | [0.9]: https://github.com/bheisig/i-doit-cli/compare/0.8...0.9 156 | [0.8]: https://github.com/bheisig/i-doit-cli/compare/0.7...0.8 157 | [0.7]: https://github.com/bheisig/i-doit-cli/compare/0.6...0.7 158 | [0.6]: https://github.com/bheisig/i-doit-cli/compare/0.5...0.6 159 | [0.5]: https://github.com/bheisig/i-doit-cli/compare/0.4...0.5 160 | [0.4]: https://github.com/bheisig/i-doit-cli/compare/0.3...0.4 161 | [0.3]: https://github.com/bheisig/i-doit-cli/compare/0.2...0.3 162 | [0.2]: https://github.com/bheisig/i-doit-cli/compare/0.1...0.2 163 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by [contacting the project team][mail]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 44 | 45 | [homepage]: https://www.contributor-covenant.org 46 | [mail]: mailto:benjamin@heisig.name 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors welcome! 2 | 3 | Thank you very much for your interest in this project! There are plenty of ways you can support us. :-) 4 | 5 | ## Code of Conduct 6 | 7 | We like you to read and follow our [code of conduct](CODE_OF_CONDUCT.md) before contributing. Thank you. 8 | 9 | ## Use it 10 | 11 | The best and (probably) easiest way to help is to use this library in your own projects. It would be very nice to share your thoughts with us. We love to hear from you. 12 | 13 | If you have questions how to use it properly read the [documentation](README.md) carefully. 14 | 15 | ## Report bugs 16 | 17 | If you find something strange please report it to [our issue tracker][issues]. 18 | 19 | ## Make a wish 20 | 21 | Of course, there are some features in the pipeline. However, if you have good ideas how to improve this application please let us know! Write a feature request [in our issue tracker][issues]. 22 | 23 | ## Setup a development environment 24 | 25 | If you like to contribute source code, documentation snippets, self-explaining examples or other useful bits, fork this repository, setup the environment and make a pull request. 26 | 27 | ~~~ {.bash} 28 | git clone https://github.com/bheisig/i-doit-cli.git 29 | ~~~ 30 | 31 | If you have a GitHub account create a fork first and then clone the repository. 32 | 33 | After that, change to your cloned repository and setup the environment with Composer: 34 | 35 | ~~~ {.bash} 36 | cd i-doit-cli/ 37 | composer install 38 | ~~~ 39 | 40 | Now it is the time to do your stuff. Do not forget to commit your changes. When you are done consider to make a pull requests. 41 | 42 | Notice, that any of your contributions merged into this repository will be [licensed under the AGPLv3](LICENSE). 43 | 44 | ## Requirements 45 | 46 | Developers must meet these requirements: 47 | 48 | - See requirements mentioned in the [documentation](README.md) 49 | - [Composer](https://getcomposer.org/) 50 | - [Git](https://git-scm.com/) 51 | 52 | ## Run it! 53 | 54 | To run `idoitcli` you do not need to build a binary first. Just run: 55 | 56 | ~~~ {.bash} 57 | bin/idoitcli 58 | ~~~ 59 | 60 | ## Add your own command 61 | 62 | Each command has its own PHP class located under `src/Command`. There is a dummy class you can use as a skeleton. Copy it: 63 | 64 | ~~~ {.bash} 65 | cd src/Command/ 66 | cp Dummy.php MyCommand.php 67 | ~~~ 68 | 69 | Note, that the file name must be in camel-case and it ends with `.php`. 70 | 71 | Edit this file with your favorite editor. The class name must be the same as the file name (without the file extension). The entry point is the public method `execute()`. 72 | 73 | Next step is to register your new command. This is done inside `bin/idoitcli.php`. Add something like this: 74 | 75 | ~~~ {.php} 76 | $app 77 | ->addCommand( 78 | 'my-command', 79 | __NAMESPACE__ . '\\Command\\MyCommand', 80 | 'Dummy command to print "Hello, World!"' 81 | ); 82 | ~~~ 83 | 84 | Now you are able to execute your command: 85 | 86 | ~~~ {.bash} 87 | bin/idoitcli my-command 88 | ~~~ 89 | 90 | Further steps: 91 | 92 | - Add options to your command with `$app->addOption()` 93 | - Overwrite public method `showUsage()` to print usage with option `--help` 94 | 95 | ## Release new version 96 | 97 | …to the public. You need commit rights for this repository. 98 | 99 | 1. Bump version: `composer config extra.version ` 100 | 3. Keep [`CHANGELOG.md`](CHANGELOG.md) up-to-date 101 | 4. Commit changes: `git commit -a -m "Bump version to $(composer config extra.version)"` 102 | 5. Perform some tests, for example `composer ci` 103 | 6. Build binary file: `composer build` 104 | 7. Create distribution tarball: `composer dist` 105 | 8. Build PHIVE files: `composer phive` 106 | 9. Create Git tag: `git tag -s -a -m "Release version $(composer config extra.version)" $(composer config extra.version)` 107 | 10. Push changes: `git push --follow-tags` 108 | 11. Create new release on GitHub based on the last tag 109 | 12. Upload these files and add them to the release: 110 | - Distribution tarball: `idoitcli-.tar.gz` 111 | - Binaray file: `idoitcli` 112 | - PHIVE files: `idoitcli.phar`, `idoitcli.phar.asc` 113 | 13. Cleanup project directory: `composer clean` 114 | 115 | If any step produces an error please think twice before releasing. ;-) 116 | 117 | ## Create OS-related packages 118 | 119 | At the moment, only Debian GNU/Linux is supported: 120 | 121 | 1. Make sure you have [`fpm`](https://github.com/jordansissel/fpm) installed 122 | 2. Build binary file: `composer build` 123 | 3. Create .deb package file: `composer deb` 124 | 125 | This results in a file `idoitcli__all.deb`. 126 | 127 | ## Composer scripts 128 | 129 | This project comes with some useful composer scripts: 130 | 131 | | Command | Description | 132 | | ----------------------------- | --------------------------------------------------------- | 133 | | `composer ci` | Perform continuous integration tasks | 134 | | `composer clean` | Cleanup project directory | 135 | | `composer build` | Create a binary | 136 | | `composer deb` | Create a Debian GNU/Linux package | 137 | | `composer dist` | Create a distribution tarball | 138 | | `composer find-forbidden` | Find forbidden words in source code | 139 | | `composer gitstats` | Create Git statistics | 140 | | `composer gource` | Visualize Git history | 141 | | `composer is-built` | Test whether binary is already built | 142 | | `composer lint` | Perform all lint checks | 143 | | `composer lint-php` | Check syntax of PHP files | 144 | | `composer lint-json` | Check syntax of JSON files | 145 | | `composer lint-xml` | Check syntax of XML files | 146 | | `composer lint-yaml` | Check syntax of YAML files | 147 | | `composer phive` | Build PHIVE files | 148 | | `composer phpcompatibility` | Run PHP compatibility checks | 149 | | `composer phpcpd` | Detect copy/paste in source code | 150 | | `composer phpcs` | Detect violations of defined coding standards | 151 | | `composer phploc` | Print source code statistics | 152 | | `composer phpmd` | Detect mess in source code | 153 | | `composer phpmnd` | Detect magic numbers in source code | 154 | | `composer phpstan` | Analyze source code | 155 | | `composer security-checker` | Look for dependencies with known security vulnerabilities | 156 | | `composer system-check` | Run some system checks | 157 | | `composer test` | Perform some tests with the built binary | 158 | 159 | For example, execute `composer ci`. 160 | 161 | ## Donate 162 | 163 | Last but not least, if you think this project is useful for your daily work, consider a donation. What about a beer? 164 | 165 | ## Further reading 166 | 167 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 168 | 169 | [issues]: https://github.com/bheisig/i-doit-cli/issues 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i-doit CLI tool 2 | 3 | Access your CMDB on the command line interface 4 | 5 | [![Latest Stable Version](https://img.shields.io/packagist/v/bheisig/idoitcli.svg)](https://packagist.org/packages/bheisig/idoitcli) 6 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/) 7 | [![Build Status](https://travis-ci.org/bheisig/i-doit-cli.svg?branch=master)](https://travis-ci.org/bheisig/i-doit-cli) 8 | 9 | ## About 10 | 11 | [i-doit](https://i-doit.com) is a software application for IT documentation and a CMDB (Configuration Management Database). This application is very useful to collect all your knowledge about the IT infrastructure you are dealing with. i-doit is a Web application and [has an exhausting API](https://kb.i-doit.com/pages/viewpage.action?pageId=37355644) which is very useful to automate your infrastructure. 12 | 13 | This application provides a simple, but powerful command line interface to access your CMDB data stored in i-doit. 14 | 15 | ## Features 16 | 17 | * Read information about objects their types and even their attributes 18 | * Find your needle in the haystack called CMDB 19 | * Do you need a free IP address in a particular subnet? This app suggests one for you. 20 | * Stress your system: auto-generate thousands of objects 21 | 22 | ## Requirements 23 | 24 | Before using this app your system must meet the following requirements: 25 | 26 | * Of course, i-doit pro/open, version 1.13 or higher 27 | * i-doit API add-on, version 1.10.3 or higher 28 | * Any POSIX operating system (GNU/Linux, *BSD, MacOS) or Windows 29 | * PHP >= 7.1 (7.3 is recommended) 30 | * PHP extensions `calendar`, `cli`, `cURL`, `json`, `phar` and `zlib` 31 | * PHP extension `pcntl` is optional, but highly recommended (non-Windows only) 32 | 33 | ## Install and update 34 | 35 | You have two good options to download and install this application: 36 | 37 | * Download any stable release (**recommended**) 38 | * Use [PHIVE](https://phar.io/) 39 | 40 | ### Download release 41 | 42 | Download the latest stable version of the binary `idoitcli` [from the release site](https://github.com/bheisig/i-doit-cli/releases). Then install it system-wide: 43 | 44 | ~~~ {.bash} 45 | curl -OL https://github.com/bheisig/i-doit-cli/releases/download/0.7/idoitcli 46 | chmod 755 idoitcli 47 | sudo mv idoitcli /usr/local/bin/ 48 | ~~~ 49 | 50 | To be up-to-date just repeat the steps above. 51 | 52 | ### Use PHIVE 53 | 54 | With [PHIVE](https://phar.io/) you are able to download and install PHAR files on your system. Additionally, it will verify the SHA1 and GPG signatures which is highly recommended. If you have PHIVE already installed you can fetch the latest version of this application: 55 | 56 | ~~~ {.bash} 57 | sudo phive install --global bheisig/i-doit-cli 58 | ~~~ 59 | 60 | This will install an executable binary to `/usr/bin/idoitcli`. 61 | 62 | If a new release is available you can perform an update: 63 | 64 | ~~~ {.bash} 65 | sudo phive update --global 66 | ~~~ 67 | 68 | ## Usage 69 | 70 | Just run the application to show the basic usage: 71 | 72 | ~~~ {.bash} 73 | idoitcli 74 | ~~~ 75 | 76 | ## First steps 77 | 78 | This application caches a lot locally to give you the best user experience. Run the `init` command: 79 | 80 | ~~~ {.bash} 81 | idoitcli init 82 | ~~~ 83 | 84 | Some simple questions will be asked how to access your i-doit installation. Next step is to create cache files: 85 | 86 | ~~~ {.bash} 87 | idoitcli cache 88 | ~~~ 89 | 90 | After that some files will be created in your home directory under `~/.idoitcli/`: Each i-doit installation has its own cache files under `data/`. Your user-defined configuration file is called `config.json`. 91 | 92 | You may check your current status by running: 93 | 94 | ~~~ {.bash} 95 | idoitcli status 96 | ~~~ 97 | 98 | This gives you some basic information about your i-doit installation, your settings and your user. 99 | 100 | ## Access your CMDB data 101 | 102 | This is probably the best part: Read information about objects, their types and even attributes. 103 | 104 | List all object types: 105 | 106 | ~~~ {.bash} 107 | idoitcli read 108 | idoitcli read / 109 | ~~~ 110 | 111 | List server objects: 112 | 113 | ~~~ {.bash} 114 | idoitcli read server/ 115 | idoitcli read server/host.example.net 116 | idoitcli read server/*.example.net 117 | idoitcli read server/host.*.net 118 | idoitcli read server/*.*.net 119 | idoitcli read server/host* 120 | idoitcli read server/* 121 | ~~~ 122 | 123 | Show common information about server "host.example.net": 124 | 125 | ~~~ {.bash} 126 | idoitcli read server/host.example.net 127 | ~~~ 128 | 129 | Show common information about object identifier "42": 130 | 131 | ~~~ {.bash} 132 | idoitcli read 42 133 | ~~~ 134 | 135 | Show common information about one or more objects by their titles: 136 | 137 | ~~~ {.bash} 138 | idoitcli read *.example.net 139 | idoitcli read host.*.net 140 | idoitcli read *.*.net 141 | idoitcli read host* 142 | ~~~ 143 | 144 | List assigned categories: 145 | 146 | ~~~ {.bash} 147 | idoitcli read server/host.example.net/ 148 | ~~~ 149 | 150 | Show values from category "model" for this server: 151 | 152 | ~~~ {.bash} 153 | idoitcli read server/host.example.net/model 154 | ~~~ 155 | 156 | Show values from category "model" for one or more servers: 157 | 158 | ~~~ {.bash} 159 | idoitcli read server/*.example.net/model 160 | idoitcli read server/host.*.net/model 161 | idoitcli read server/*.*.net/model 162 | idoitcli read server/host*/model 163 | idoitcli read server/*/model 164 | ~~~ 165 | 166 | Or just show the name of the model: 167 | 168 | ~~~ {.bash} 169 | idoitcli read server/host.example.net/model/model 170 | ~~~ 171 | 172 | List available attributes for category "model": 173 | 174 | ~~~ {.bash} 175 | idoitcli read server/host.example.net/model/ 176 | ~~~ 177 | 178 | You may leave the object type empty for specific objects, for example: 179 | 180 | ~~~ {.bash} 181 | idoitcli read host.example.net/model 182 | ~~~ 183 | 184 | **Notice:** These examples work great with unique names. That is why it is common practice to give objects unique titles that are not in conflict with object types and categories. 185 | 186 | ## Show everything about an object 187 | 188 | ~~~ {.bash} 189 | idoitcli show myserver 190 | idoitcli show "My Server" 191 | idoitcli show 42 192 | ~~~ 193 | 194 | ## Find your data 195 | 196 | Find your needle in the haystack called CMDB: 197 | 198 | ~~~ {.bash} 199 | idoitcli search myserver 200 | idoitcli search "My Server" 201 | ~~~ 202 | 203 | ## Show the next free IPv4 address 204 | 205 | Get the next free IPv4 address for a particular subnet: 206 | 207 | ~~~ {.bash} 208 | idoitcli nextip SUBNET 209 | ~~~ 210 | 211 | `SUBNET` may be the object title or its identifier. 212 | 213 | ## Auto-generate objects 214 | 215 | For testing purposes stress your i-doit installation and let the app create thousands of objects, attributes and relations between objects: 216 | 217 | ~~~ {.bash} 218 | idoitcli -c FILE random 219 | ~~~ 220 | 221 | There are some examples located under `docs/`. 222 | 223 | ## Update the caches 224 | 225 | If your CMDB configuration has changed you need to re-create the cache files by running the `cache` command: 226 | 227 | ~~~ {.bash} 228 | idoitcli cache 229 | ~~~ 230 | 231 | ## Playground 232 | 233 | Perform self-defined API requests – pass request as argument: 234 | 235 | ~~~ {.bash} 236 | idoitcli call '{"version": "2.0","method": "idoit.version","params": {"apikey": "c1ia5q","language": "en"},"id": 1}' 237 | ~~~ 238 | 239 | Pipe request: 240 | 241 | ~~~ {.bash} 242 | echo '{"version": "2.0","method": "idoit.version","params": {"apikey": "c1ia5q","language": "en"},"id": 1}' | idoitcli call 243 | ~~~ 244 | 245 | Read request from file: 246 | 247 | ~~~ {.bash} 248 | cat request.txt | idoitcli call 249 | ~~~ 250 | 251 | Read request from standard input (double-enter to execute): 252 | 253 | ~~~ {.bash} 254 | idoitcli call 255 | ~~~ 256 | 257 | ## Configuration 258 | 259 | There are some ways to set your configurations settings: 260 | 261 | 1. System-wide settings are stored under `/etc/idoitcli/config.json`. 262 | 2. User-defined settings are stored under `~/.idoitcli/config.json`. 263 | 3. Pass your run-time settings by using the options `-c` or `--config`. 264 | 265 | Keep in mind this order matters. Each file is optional. Any combination is possible. Furthermore, to include even more configuration files you can pass the options `-c` and `--config` as often as you like. 266 | 267 | The configuration files are JSON-formatted. 268 | 269 | ## Contribute 270 | 271 | Please, report any issues to [our issue tracker](https://github.com/bheisig/i-doit-cli/issues). Pull requests and OS distribution packages are very welcomed. For further information, see file [`CONTRIBUTING.md`](CONTRIBUTING.md). 272 | 273 | ## Copyright & License 274 | 275 | Copyright (C) 2016-19 [Benjamin Heisig](https://benjamin.heisig.name/) 276 | 277 | Licensed under the [GNU Affero GPL version 3 or later (AGPLv3+)](https://gnu.org/licenses/agpl.html). This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 278 | 279 | [List of first names and gender:](https://www.heise.de/ct/ftp/07/17/182/) Copyright (C) 2007-2008 Jörg Michael, licensed under [GNU Free Documentation License](https://www.gnu.org/licenses/fdl.html) 280 | 281 | [List of surnames:](https://github.com/HBehrens/phonet4n/blob/master/src/Tests/data/nachnamen.txt) Copyright (C) Heiko Behrens, lisenced under [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0) 282 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for taking time reading the following information carefully. Your [contribution](CONTRIBUTING.md) is highly welcomed! 4 | 5 | ## Supported versions 6 | 7 | Only the **latest stable release** is supported with security updates. 8 | 9 | ## Reporting a vulnerability 10 | 11 | I highly encourage you to follow the principles of a **[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure)**: 12 | 13 | - Do not publish your research before contacting me first. 14 | - Write me an e-mail: 15 | - Sign and encrypt your e-mail with OpenPGP/GnuPG. 16 | - Do not open a public issue until my confirmation. 17 | - If the vulnerability is accepted please give me at least 4 weeks to release proper security updates. 18 | - After the security updates are released feel free to publish your research. A CVE is highly appreciated. 19 | 20 | **Note:** This is a private side-project, maintained in my spare time. Therefore, I can't promise to answer you within three days but I try to respond within 3-5 days. 21 | 22 | Thank you. 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | 3 | platform: 4 | - x64 5 | 6 | clone_depth: 5 7 | 8 | environment: 9 | matrix: 10 | - PHP_VERSION: '7.1' 11 | XDEBUG_VERSION: '2.6.1-7.1-vc14-nts-x86_64' 12 | - PHP_VERSION: '7.2' 13 | XDEBUG_VERSION: '2.6.1-7.2-vc15-nts-x86_64' 14 | - PHP_VERSION: '7.3' 15 | XDEBUG_VERSION: '2.7.0-7.3-vc15-nts-x86_64' 16 | 17 | cache: 18 | - '%LOCALAPPDATA%\Composer\files -> composer.lock' 19 | - composer.phar 20 | - C:\ProgramData\chocolatey\bin -> appveyor.yml 21 | - C:\ProgramData\chocolatey\lib -> appveyor.yml 22 | - c:\tools\php -> appveyor.yml 23 | 24 | init: 25 | - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% 26 | - SET COMPOSER_NO_INTERACTION=1 27 | - SET PHP=1 28 | - SET ANSICON=121x90 (121x90) 29 | 30 | install: 31 | - IF EXIST c:\tools\php (SET PHP=0) 32 | - ps: appveyor-retry cinst --params '""/InstallDir:C:\tools\php""' --ignore-checksums -y php --version ((choco search php --exact --all-versions -r | select-string -pattern $env:PHP_VERSION | sort { [version]($_ -split '\|' | select -last 1) } -Descending | Select-Object -first 1) -replace '[php|]','') 33 | - cd c:\tools\php 34 | - IF %PHP%==1 copy php.ini-production php.ini /Y 35 | - IF %PHP%==1 echo date.timezone="UTC" >> php.ini 36 | - IF %PHP%==1 echo extension_dir=ext >> php.ini 37 | - IF %PHP%==1 echo extension=php_bz2.dll >> php.ini 38 | - IF %PHP%==1 echo extension=php_curl.dll >> php.ini 39 | - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini 40 | - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini 41 | - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini 42 | - IF %PHP%==1 echo extension=php_xsl.dll >> php.ini 43 | - IF %PHP%==1 curl -fsS -o c:\tools\php\ext\php_xdebug-%XDEBUG_VERSION%.dll https://xdebug.org/files/php_xdebug-%XDEBUG_VERSION%.dll 44 | - IF %PHP%==1 echo zend_extension=php_xdebug-%XDEBUG_VERSION%.dll >> php.ini 45 | - IF %PHP%==1 curl -fsS -o c:\tools\php\extras\ssl\cacert.pem https://curl.haxx.se/ca/cacert.pem 46 | - IF %PHP%==1 echo curl.cainfo="C:\tools\php\extras\ssl\cacert.pem" >> php.ini 47 | - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat 48 | - appveyor-retry appveyor DownloadFile https://getcomposer.org/composer.phar 49 | - cd %APPVEYOR_BUILD_FOLDER% 50 | - appveyor-retry composer install --no-progress --profile 51 | - appveyor-retry composer self-update --update-keys 52 | 53 | test_script: 54 | - cd %APPVEYOR_BUILD_FOLDER% 55 | - ps: composer win-ci 56 | - php idoitcli --help 57 | - php idoitcli --version 58 | 59 | notifications: 60 | - provider: Email 61 | on_build_success: false 62 | on_build_failure: true 63 | on_build_status_changed: true 64 | -------------------------------------------------------------------------------- /bin/idoitcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | . 19 | * 20 | * @author Benjamin Heisig 21 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 22 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 23 | * @link https://github.com/bheisig/i-doit-cli 24 | */ 25 | 26 | require_once('idoitcli.php'); 27 | -------------------------------------------------------------------------------- /bin/idoitcli-dbg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export XDEBUG_CONFIG="idekey=idoitcli remote_enable=1 remote_host=localhost profiler_enable=0 remote_connect_back=0 remote_mode=req" 4 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" 5 | /usr/bin/env php "$DIR"/idoitcli.php "$@" 6 | -------------------------------------------------------------------------------- /bin/idoitcli.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli; 28 | 29 | use bheisig\idoitcli\Command\Network; 30 | use bheisig\idoitcli\Command\Rack; 31 | use \Exception; 32 | use bheisig\cli\App; 33 | use bheisig\cli\IO; 34 | 35 | try { 36 | require_once __DIR__ . '/../vendor/autoload.php'; 37 | 38 | (new App()) 39 | ->addConfigSettings([ 40 | 'appDir' => __DIR__ . '/..', 41 | 'baseDir' => (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 42 | $_SERVER['LOCALAPPDATA'] . '\\idoitcli' : 43 | $_SERVER['HOME'] . '/.idoitcli', 44 | 'dataDir' => (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 45 | $_SERVER['LOCALAPPDATA'] . '\\idoitcli\\data' : 46 | $_SERVER['HOME'] . '/.idoitcli/data' 47 | ]) 48 | ->addCommand( 49 | 'cache', 50 | __NAMESPACE__ . '\\Command\\Cache', 51 | 'Create cache files needed for faster processing' 52 | ) 53 | ->addCommand( 54 | 'call', 55 | __NAMESPACE__ . '\\Command\\Call', 56 | 'Perform self-defined API requests' 57 | ) 58 | ->addCommand( 59 | 'categories', 60 | __NAMESPACE__ . '\\Command\\Categories', 61 | 'Print a list of available categories' 62 | ) 63 | ->addCommand( 64 | 'create', 65 | __NAMESPACE__ . '\\Command\\Create', 66 | 'Create new object or category entry' 67 | ) 68 | ->addCommand( 69 | 'fixip', 70 | __NAMESPACE__ . '\\Command\\FixIP', 71 | 'Assign IPs to subnets' 72 | ) 73 | // Extent basic command: 74 | ->addCommand( 75 | 'init', 76 | __NAMESPACE__ . '\\Command\\Init', 77 | 'Create/update user-defined or system-wide configuration settings' 78 | ) 79 | ->addCommand( 80 | 'log', 81 | __NAMESPACE__ . '\\Command\\Log', 82 | 'Add entry to i-doit logbook' 83 | ) 84 | ->addCommand( 85 | 'logs', 86 | __NAMESPACE__ . '\\Command\\Logs', 87 | 'Print entries from i-doit logbook' 88 | ) 89 | ->addCommand( 90 | 'network', 91 | __NAMESPACE__ . '\\Command\\Network', 92 | 'Print list of IPv4 addresses' 93 | ) 94 | ->addCommand( 95 | 'nextip', 96 | __NAMESPACE__ . '\\Command\\NextIP', 97 | 'Fetch the next free IPv4 address' 98 | ) 99 | ->addCommand( 100 | 'rack', 101 | __NAMESPACE__ . '\\Command\\Rack', 102 | 'Visualize hardware rack' 103 | ) 104 | ->addCommand( 105 | 'random', 106 | __NAMESPACE__ . '\\Command\\Random', 107 | 'Create randomized data' 108 | ) 109 | ->addCommand( 110 | 'read', 111 | __NAMESPACE__ . '\\Command\\Read', 112 | 'Fetch information from your CMDB' 113 | ) 114 | ->addCommand( 115 | 'save', 116 | __NAMESPACE__ . '\\Command\\Save', 117 | 'Create/update CMDB objects and their category entries' 118 | ) 119 | ->addCommand( 120 | 'search', 121 | __NAMESPACE__ . '\\Command\\Search', 122 | 'Find your needle in the haystack called CMDB' 123 | ) 124 | ->addCommand( 125 | 'show', 126 | __NAMESPACE__ . '\\Command\\Show', 127 | 'Show everything about an object' 128 | ) 129 | ->addCommand( 130 | 'status', 131 | __NAMESPACE__ . '\\Command\\Status', 132 | 'Current status information' 133 | ) 134 | ->addCommand( 135 | 'types', 136 | __NAMESPACE__ . '\\Command\\Types', 137 | 'Print a list of available object types and group them' 138 | ) 139 | // Common options: 140 | ->addOption( 141 | 'y', 142 | 'yes', 143 | App::NO_VALUE 144 | ) 145 | // Used by command "save": 146 | ->addOption( 147 | 'a', 148 | 'attribute', 149 | App::OPTION_NOT_REQUIRED 150 | ) 151 | // Used by command "log": 152 | ->addOption( 153 | 'm', 154 | 'message', 155 | App::OPTION_NOT_REQUIRED 156 | ) 157 | // Used by command "logs": 158 | ->addOption( 159 | 'f', 160 | 'follow', 161 | App::NO_VALUE 162 | ) 163 | ->addOption( 164 | null, 165 | 'id', 166 | App::OPTION_NOT_REQUIRED 167 | ) 168 | ->addOption( 169 | 'n', 170 | 'number', 171 | App::OPTION_NOT_REQUIRED 172 | ) 173 | ->addOption( 174 | null, 175 | 'since', 176 | App::OPTION_NOT_REQUIRED 177 | ) 178 | ->addOption( 179 | null, 180 | 'title', 181 | App::OPTION_NOT_REQUIRED 182 | ) 183 | ->addOption( 184 | null, 185 | 'type', 186 | App::OPTION_NOT_REQUIRED 187 | ) 188 | // Used by command "network": 189 | ->addOption( 190 | null, 191 | Network::OPTION_FREE, 192 | App::NO_VALUE 193 | ) 194 | ->addOption( 195 | null, 196 | Network::OPTION_USED, 197 | App::NO_VALUE 198 | ) 199 | // Used by command "rack": 200 | ->addOption( 201 | null, 202 | Rack::OPTION_FRONT, 203 | App::NO_VALUE 204 | ) 205 | ->addOption( 206 | null, 207 | Rack::OPTION_BACK, 208 | App::NO_VALUE 209 | ) 210 | ->addOption( 211 | null, 212 | Rack::OPTION_SKIP_EMPTY_UNITS, 213 | App::NO_VALUE 214 | ) 215 | ->run(); 216 | } catch (Exception $e) { 217 | IO::err($e->getMessage()); 218 | 219 | exit(255); 220 | } 221 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bheisig/idoitcli", 3 | "description": "Access your CMDB on the command line interface", 4 | "type": "project", 5 | "keywords": ["i-doit", "cmdb", "it documentation", "cli", "api", "json-rpc"], 6 | "homepage": "https://github.com/bheisig/i-doit-cli", 7 | "license": "AGPL-3.0+", 8 | "authors": [ 9 | { 10 | "name": "Benjamin Heisig", 11 | "email": "benjamin@heisig.name", 12 | "homepage": "https://benjamin.heisig.name/", 13 | "role": "Developer" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/bheisig/i-doit-cli/issues", 18 | "source": "https://github.com/bheisig/i-doit-cli" 19 | }, 20 | "require": { 21 | "php": ">=7.1.0", 22 | "ext-Phar": "*", 23 | "ext-zlib": "*", 24 | "bheisig/cli": "@dev", 25 | "bheisig/idoitapi": "@dev" 26 | }, 27 | "require-dev": { 28 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 29 | "j13k/yaml-lint": "^1.1", 30 | "jakub-onderka/php-parallel-lint": "^1.0", 31 | "macfja/phar-builder": "^0.2.6", 32 | "phpcompatibility/php-compatibility": "*", 33 | "phploc/phploc": "^4.0", 34 | "phpmd/phpmd" : "@stable", 35 | "phpstan/phpstan": "^0.11.6", 36 | "phpunit/phpunit": "^7", 37 | "povils/phpmnd": "^2", 38 | "roave/security-advisories": "dev-master", 39 | "sclable/xml-lint": "^0.2.4", 40 | "sebastian/phpcpd": "^4.1.0", 41 | "seld/jsonlint": "^1.7", 42 | "sensiolabs/security-checker": "^5", 43 | "sllh/composer-lint": "^1.0", 44 | "squizlabs/php_codesniffer": "*" 45 | }, 46 | "suggest": { 47 | "ext-xdebug": "Needed for code coverage with phpunit" 48 | }, 49 | "prefer-stable" : false, 50 | "autoload": { 51 | "psr-4": { 52 | "bheisig\\idoitcli\\": "src/" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "bheisig\\idoitcli\\tests\\": "tests/" 58 | } 59 | }, 60 | "bin": ["bin/idoitcli"], 61 | "config": { 62 | "process-timeout": 86400, 63 | "sllh-composer-lint": { 64 | "php": false, 65 | "type": true, 66 | "minimum-stability": true, 67 | "version-constraints": true 68 | }, 69 | "sort-packages": true 70 | }, 71 | "scripts": { 72 | "ci": [ 73 | "@composer system-check", 74 | "@composer lint", 75 | "@composer find-forbidden", 76 | "@composer security-checker", 77 | "@composer phpcompatibility", 78 | "@composer phpcpd", 79 | "@composer phpcs", 80 | "@composer phpstan", 81 | "@composer build", 82 | "@composer test", 83 | "@composer dist", 84 | "@composer clean" 85 | ], 86 | "clean": [ 87 | "rm -f `composer config extra.name`", 88 | "rm -rf *.tar.gz ./`composer config extra.name`-`composer config extra.version`/", 89 | "rm -f `composer config extra.name`.phar `composer config extra.name`.phar.asc" 90 | ], 91 | "build": [ 92 | "@composer dump-autoload --classmap-authoritative", 93 | "php -d phar.readonly=0 ./vendor/bin/phar-builder package --quiet composer.json", 94 | "mv `composer config extra.name`.phar `composer config extra.name`", 95 | "chmod +x `composer config extra.name`", 96 | "@composer dump-autoload" 97 | ], 98 | "deb": [ 99 | "@composer is-built", 100 | "touch .dummy", 101 | "rm -f `composer config extra.name`_`composer config extra.version`_all.deb", 102 | "fpm -s dir -t deb -n `composer config extra.name` -v `composer config extra.version` -a all --license \"`composer config license`\" -m \"Benjamin Heisig \" --vendor \"Benjamin Heisig \" --description \"`composer config description`\" --url \"`composer config homepage`\" -d php -d php-cli -d php-common -d php-bz2 -d php-curl -d php-json `composer config extra.name`=/usr/bin/`composer config extra.name` .dummy=/etc/`composer config extra.name`", 103 | "rm .dummy", 104 | "lintian `composer config extra.name`_`composer config extra.version`_all.deb" 105 | ], 106 | "dist": [ 107 | "@composer is-built", 108 | "rm -rf ./`composer config extra.name`-`composer config extra.version`/", 109 | "mkdir ./`composer config extra.name`-`composer config extra.version`/", 110 | "cp -r `composer config extra.name` ./`composer config extra.name`-`composer config extra.version`/", 111 | "cp -r CHANGELOG.md ./`composer config extra.name`-`composer config extra.version`/", 112 | "cp -r CONTRIBUTING.md ./`composer config extra.name`-`composer config extra.version`/", 113 | "cp -r LICENSE ./`composer config extra.name`-`composer config extra.version`/", 114 | "cp -r README.md ./`composer config extra.name`-`composer config extra.version`/", 115 | "tar czf `composer config extra.name`-`composer config extra.version`.tar.gz ./`composer config extra.name`-`composer config extra.version`/", 116 | "rm -r ./`composer config extra.name`-`composer config extra.version`/" 117 | ], 118 | "find-forbidden": [ 119 | "! grep -rEn \"(var_dump|die|exit)\\(\" src/*" 120 | ], 121 | "gitstats": "gitstats -c project_name=`composer config extra.name` . gitstats", 122 | "gource": "gource -1280x720 --seconds-per-day 3 --auto-skip-seconds 1 --title `composer config extra.name`", 123 | "is-built": "test -x `composer config extra.name` || ( echo 'Run \"composer build\" first' && exit 1 )", 124 | "lint": [ 125 | "@composer lint-php", 126 | "@composer lint-json", 127 | "@composer lint-xml", 128 | "@composer lint-yaml" 129 | ], 130 | "lint-json": "./vendor/bin/jsonlint composer.json config/default.json config/schema.json config/template.json", 131 | "lint-php": "./vendor/bin/parallel-lint --exclude vendor --blame .", 132 | "lint-xml": "./vendor/bin/xmllint --recursive --exclude=vendor --skip-xsd --verbose .", 133 | "lint-yaml": [ 134 | "./vendor/bin/yaml-lint .travis.yml", 135 | "./vendor/bin/yaml-lint appveyor.yml" 136 | ], 137 | "phive": [ 138 | "@composer is-built", 139 | "cp `composer config extra.name` `composer config extra.name`.phar", 140 | "gpg --detach-sign --output `composer config extra.name`.phar.asc `composer config extra.name`.phar" 141 | ], 142 | "phpcompatibility": "./vendor/bin/phpcs -p --colors --extensions=php --standard=PHPCompatibility --runtime-set testVersion 7.1 src/", 143 | "phpcpd": "./vendor/bin/phpcpd src/", 144 | "phpcs": "./vendor/bin/phpcs --extensions=php --standard=PSR1,PSR2 --exclude=PSR2.Classes.ClassDeclaration,Squiz.Functions.MultiLineFunctionDeclaration bin/ src/", 145 | "phploc": "./vendor/bin/phploc --exclude=vendor --exclude=docs .", 146 | "phpmd": "./vendor/bin/phpmd src text cleancode,codesize,controversial,design,naming,unusedcode", 147 | "phpmnd": "./vendor/bin/phpmnd . --non-zero-exit-on-violation --exclude=vendor", 148 | "phpstan": "./vendor/bin/phpstan analyze -l max src", 149 | "phpunit": "./vendor/bin/phpunit --configuration ./phpunit.xml --testdox", 150 | "security-checker": "./vendor/bin/security-checker security:check ./composer.lock", 151 | "system-check": [ 152 | "php --version", 153 | "php -m", 154 | "php --info | grep -E \"(max_execution_time|memory_limit)\"", 155 | "@composer --version", 156 | "@composer validate", 157 | "@composer diagnose || echo \"Ignore warnings\"", 158 | "@composer check-platform-reqs", 159 | "@composer config extra.version" 160 | ], 161 | "test": [ 162 | "@composer is-built", 163 | "./`composer config extra.name` --help", 164 | "./`composer config extra.name` --version", 165 | "for command in $(./`composer config extra.name` list 2> /dev/null | awk '{print $1}'); do echo Run \"./`composer config extra.name` $command --help\" ; ./`composer config extra.name` \"$command\" --help > /dev/null || echo \"Command $command failed with non-zero exit code\"; done" 166 | ], 167 | "win-ci": [ 168 | "@composer win-system-check", 169 | "@composer win-build" 170 | ], 171 | "win-build": [ 172 | "@composer dump-autoload --classmap-authoritative", 173 | ".\\vendor\\bin\\phar-builder.bat package composer.json", 174 | "powershell mv \"$(composer config extra.name).phar\" \"$(composer config extra.name)\"", 175 | "@composer dump-autoload" 176 | ], 177 | "win-system-check": [ 178 | "php --version", 179 | "php -m", 180 | "@composer --version", 181 | "@composer validate", 182 | "@composer diagnose & exit 0", 183 | "@composer check-platform-reqs", 184 | "@composer config extra.version" 185 | ] 186 | }, 187 | "scripts-descriptions": { 188 | "ci": "Perform continuous integration tasks", 189 | "clean": "Cleanup project directory", 190 | "build": "Create a binary", 191 | "deb": "Create a Debian GNU/Linux package", 192 | "dist": "Create a distribution tarball", 193 | "find-forbidden": "Find forbidden words in source code", 194 | "gitstats": "Create Git statistics", 195 | "gource": "Visualize Git history", 196 | "is-built": "Test whether binary is already built", 197 | "lint": "Perform all lint checks", 198 | "lint-php": "Check syntax of PHP files", 199 | "lint-json": "Check syntax of JSON files", 200 | "lint-xml": "Check syntax of XML files", 201 | "lint-yaml": "Check syntax of YAML files", 202 | "phive": "Build PHIVE files", 203 | "phpcompatibility": "Run PHP compatibility checks", 204 | "phpcpd": "Detect copy/paste in source code", 205 | "phpcs": "Detect violations of defined coding standards", 206 | "phploc": "Print source code statistics", 207 | "phpmd": "Detect mess in source code", 208 | "phpmnd": "Detect magic numbers in source code", 209 | "phpstan": "Analyze source code", 210 | "phpunit": "Perform unit tests", 211 | "security-checker": "Look for dependencies with known security vulnerabilities", 212 | "system-check": "Run some system checks", 213 | "test": "Perform some tests with the built binary" 214 | }, 215 | "extra": { 216 | "name": "idoitcli", 217 | "version": "0.9", 218 | "phar-builder": { 219 | "skip-shebang": false, 220 | "include-dev": false, 221 | "entry-point": "bin/idoitcli.php", 222 | "compression": "GZip", 223 | "name": "idoitcli.phar", 224 | "output-dir": "./", 225 | "include": ["config"] 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /config/applications.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "amount": 1000, 4 | "objectType": "C__OBJTYPE__APPLICATION", 5 | "prefix": "app-" 6 | }, 7 | "licenses": { 8 | "amount": 10, 9 | "objectType": "C__OBJTYPE__LICENCE", 10 | "prefix": "license-" 11 | }, 12 | "laptops": { 13 | "amount": 700, 14 | "objectType": "C__OBJTYPE__CLIENT", 15 | "prefix": "laptop-" 16 | }, 17 | "installApplications": { 18 | "min": 100, 19 | "max": 1000, 20 | "addLicense": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/dcim.json: -------------------------------------------------------------------------------- 1 | { 2 | "racks": { 3 | "amount": 15, 4 | "prefix": "rack-", 5 | "rackUnits": 47, 6 | "verticalSlots": 2 7 | }, 8 | "servers": { 9 | "amount": 100, 10 | "prefix": "srv-", 11 | "rackUnits": { 12 | "min": 1, 13 | "max": 4 14 | }, 15 | "model": [ 16 | { 17 | "manufacturer": "Hell", 18 | "title": "PowerBadge R415" 19 | }, 20 | { 21 | "manufacturer": "Venolo", 22 | "title": "y3650" 23 | }, 24 | { 25 | "manufacturer": "Hans-Peter", 26 | "title": "ProLiar DL585" 27 | } 28 | ], 29 | "cpu": { 30 | "amount": 2, 31 | "manufacturer": "Amdtel", 32 | "type": "Neon", 33 | "cores": 12, 34 | "frequency": 2.4 35 | }, 36 | "memory": { 37 | "amount": 16, 38 | "type": "DDR4 ECC", 39 | "capacity": "16 GB" 40 | }, 41 | "hdd": { 42 | "amount": 4, 43 | "raid": 6, 44 | "capacity": "4 TB" 45 | }, 46 | "power": { 47 | "amount": 2, 48 | "watt": 200 49 | } 50 | }, 51 | "density": { 52 | "rackUnits": 0.6, 53 | "racksPerRoom": { 54 | "min": 1, 55 | "max": 4 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "url": "", 4 | "key": "", 5 | "language": "en" 6 | }, 7 | "cacheLifetime": 15768000, 8 | "limitBatchRequests": 0, 9 | "types": { 10 | "countries": "C__OBJTYPE__COUNTRY", 11 | "cities": "C__OBJTYPE__CITY", 12 | "buildings": "C__OBJTYPE__BUILDING", 13 | "rooms": "C__OBJTYPE__ROOM", 14 | "racks": "C__OBJTYPE__ENCLOSURE", 15 | "subnets": "C__OBJTYPE__LAYER3_NET", 16 | "servers": "C__OBJTYPE__SERVER", 17 | "persons": "C__OBJTYPE__PERSON", 18 | "desks": "C__OBJTYPE__WORKSTATION" 19 | }, 20 | "fixip": { 21 | "blacklistedObjectTypes": [ 22 | "C__OBJTYPE__SERVICE", 23 | "C__OBJTYPE__APPLICATION", 24 | "C__OBJTYPE__LICENCE", 25 | "C__OBJTYPE__OPERATING_SYSTEM", 26 | "C__OBJTYPE__DBMS", 27 | "C__OBJTYPE__DATABASE_SCHEMA", 28 | "C__OBJTYPE__DATABASE_INSTANCE", 29 | "C__OBJTYPE__BUILDING", 30 | "C__OBJTYPE__ROOM", 31 | "C__OBJTYPE__CABLE", 32 | "C__OBJTYPE__WIRING_SYSTEM", 33 | "C__OBJTYPE__COUNTRY", 34 | "C__OBJECT_TYPE__FLOOR", 35 | "C__CMDB__OBJTYPE__CABLE_TRAY", 36 | "C__CMDB__OBJTYPE__CONDUIT", 37 | "C__OBJECT_TYPE__GROUP", 38 | "C__OBJTYPE__VEHICLE", 39 | "C__OBJTYPE__AIRCRAFT", 40 | "C__OBJTYPE__INFORMATION_DOMAIN", 41 | "C__OBJTYPE__ORGANIZATION", 42 | "C__OBJTYPE__PERSON", 43 | "C__OBJTYPE__PERSON_GROUP", 44 | "C__OBJTYPE__NAGIOS_SERVICE_TPL", 45 | "C__OBJTYPE__SIM_CARD", 46 | "C__OBJTYPE__EMERGENCY_PLAN", 47 | "C__OBJTYPE__MAINTENANCE", 48 | "C__OBJTYPE__FILE", 49 | "C__OBJTYPE__IT_SERVICE", 50 | "C__OBJTYPE__WAN", 51 | "C__OBJTYPE__LAYER3_NET", 52 | "C__OBJTYPE__LAYER2_NET", 53 | "C__OBJTYPE__SUPERNET" 54 | ], 55 | "unproperSubnets": [ 56 | "Global v4", 57 | "Global v6" 58 | ] 59 | }, 60 | "log": { 61 | "colorize": true, 62 | "verbosity": 31 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "subnets": { 3 | "10.0.1.0/24": { 4 | "type": "IPv4", 5 | "address": "10.0.1.0", 6 | "mask": "255.255.255.0" 7 | }, 8 | "10.0.2.0/24": { 9 | "type": "IPv4", 10 | "address": "10.0.2.0", 11 | "mask": "255.255.255.0" 12 | }, 13 | "10.0.3.0/24": { 14 | "type": "IPv4", 15 | "address": "10.0.3.0", 16 | "mask": "255.255.255.0" 17 | }, 18 | "10.0.4.0/24": { 19 | "type": "IPv4", 20 | "address": "10.0.4.0", 21 | "mask": "255.255.255.0" 22 | }, 23 | "10.0.5.0/24": { 24 | "type": "IPv4", 25 | "address": "10.0.5.0", 26 | "mask": "255.255.255.0" 27 | }, 28 | "10.0.6.0/24": { 29 | "type": "IPv4", 30 | "address": "10.0.6.0", 31 | "mask": "255.255.255.0" 32 | }, 33 | "10.0.7.0/24": { 34 | "type": "IPv4", 35 | "address": "10.0.7.0", 36 | "mask": "255.255.255.0" 37 | }, 38 | "10.0.8.0/24": { 39 | "type": "IPv4", 40 | "address": "10.0.8.0", 41 | "mask": "255.255.255.0" 42 | }, 43 | "10.0.9.0/24": { 44 | "type": "IPv4", 45 | "address": "10.0.9.0", 46 | "mask": "255.255.255.0" 47 | }, 48 | "10.0.10.0/24": { 49 | "type": "IPv4", 50 | "address": "10.0.10.0", 51 | "mask": "255.255.255.0" 52 | }, 53 | "10.0.11.0/24": { 54 | "type": "IPv4", 55 | "address": "10.0.11.0", 56 | "mask": "255.255.255.0" 57 | }, 58 | "10.0.12.0/24": { 59 | "type": "IPv4", 60 | "address": "10.0.12.0", 61 | "mask": "255.255.255.0" 62 | }, 63 | "10.0.13.0/24": { 64 | "type": "IPv4", 65 | "address": "10.0.13.0", 66 | "mask": "255.255.255.0" 67 | }, 68 | "10.0.14.0/24": { 69 | "type": "IPv4", 70 | "address": "10.0.14.0", 71 | "mask": "255.255.255.0" 72 | }, 73 | "10.0.15.0/24": { 74 | "type": "IPv4", 75 | "address": "10.0.15.0", 76 | "mask": "255.255.255.0" 77 | }, 78 | "10.0.16.0/24": { 79 | "type": "IPv4", 80 | "address": "10.0.16.0", 81 | "mask": "255.255.255.0" 82 | }, 83 | "10.0.17.0/24": { 84 | "type": "IPv4", 85 | "address": "10.0.17.0", 86 | "mask": "255.255.255.0" 87 | }, 88 | "10.0.18.0/24": { 89 | "type": "IPv4", 90 | "address": "10.0.18.0", 91 | "mask": "255.255.255.0" 92 | }, 93 | "10.0.19.0/24": { 94 | "type": "IPv4", 95 | "address": "10.0.19.0", 96 | "mask": "255.255.255.0" 97 | }, 98 | "10.0.20.0/24": { 99 | "type": "IPv4", 100 | "address": "10.0.20.0", 101 | "mask": "255.255.255.0" 102 | }, 103 | "10.1.0.0/16": { 104 | "type": "IPv4", 105 | "address": "10.1.0.0", 106 | "mask": "255.255.0.0" 107 | }, 108 | "10.2.0.0/16": { 109 | "type": "IPv4", 110 | "address": "10.2.0.0", 111 | "mask": "255.255.0.0" 112 | }, 113 | "10.3.0.0/16": { 114 | "type": "IPv4", 115 | "address": "10.3.0.0", 116 | "mask": "255.255.0.0" 117 | }, 118 | "10.4.0.0/16": { 119 | "type": "IPv4", 120 | "address": "10.4.0.0", 121 | "mask": "255.255.0.0" 122 | }, 123 | "10.5.0.0/16": { 124 | "type": "IPv4", 125 | "address": "10.5.0.0", 126 | "mask": "255.255.0.0" 127 | }, 128 | "10.6.0.0/16": { 129 | "type": "IPv4", 130 | "address": "10.6.0.0", 131 | "mask": "255.255.0.0" 132 | }, 133 | "10.7.0.0/16": { 134 | "type": "IPv4", 135 | "address": "10.7.0.0", 136 | "mask": "255.255.0.0" 137 | }, 138 | "10.8.0.0/16": { 139 | "type": "IPv4", 140 | "address": "10.8.0.0", 141 | "mask": "255.255.0.0" 142 | }, 143 | "10.9.0.0/16": { 144 | "type": "IPv4", 145 | "address": "10.9.0.0", 146 | "mask": "255.255.0.0" 147 | }, 148 | "10.10.0.0/16": { 149 | "type": "IPv4", 150 | "address": "10.10.0.0", 151 | "mask": "255.255.0.0" 152 | } 153 | }, 154 | "servers": { 155 | "amount": 30000, 156 | "prefix": "srv-", 157 | "ips": 4 158 | }, 159 | "density": { 160 | "subnets": 0.8 161 | } 162 | } -------------------------------------------------------------------------------- /config/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"key": "api", "type": "object", "required": true, "nodes": [ 3 | {"key": "url", "type": "string", "required": true}, 4 | {"key": "key", "type": "string", "required": true}, 5 | {"key": "username", "type": "string", "required": false, "minLength": 1}, 6 | {"key": "password", "type": "string", "required": false, "minLength": 1}, 7 | {"key": "language", "type": "string", "required": false, "values": ["en", "de"]}, 8 | {"key": "proxy", "type": "object", "required": false, "nodes": [ 9 | {"key": "type", "type": "string", "required": true, "values": ["HTTP", "SOCKS5"]}, 10 | {"key": "host", "type": "string", "required": false, "minLength": 1}, 11 | {"key": "port", "type": "integer", "required": false, "ge": 1, "le": 65536}, 12 | {"key": "username", "type": "string", "required": false, "minLength": 1}, 13 | {"key": "password", "type": "string", "required": false, "minLength": 1}, 14 | {"key": "active", "type": "boolean", "required": true} 15 | ]} 16 | ]}, 17 | {"key": "cacheLifetime", "type": "integer", "required": true, "ge": 0}, 18 | {"key": "limitBatchRequests", "type": "integer", "required": true, "ge": 0}, 19 | {"key": "types", "type": "object", "required": false, "nodes": [ 20 | {"key": "countries", "type": "string", "required": true, "minLength": 1}, 21 | {"key": "cities", "type": "string", "required": true, "minLength": 1}, 22 | {"key": "buildings", "type": "string", "required": true, "minLength": 1}, 23 | {"key": "rooms", "type": "string", "required": true, "minLength": 1}, 24 | {"key": "racks", "type": "string", "required": true, "minLength": 1}, 25 | {"key": "subnets", "type": "string", "required": true, "minLength": 1}, 26 | {"key": "servers", "type": "string", "required": true, "minLength": 1}, 27 | {"key": "persons", "type": "string", "required": true, "minLength": 1}, 28 | {"key": "desks", "type": "string", "required": true, "minLength": 1} 29 | ]}, 30 | {"key": "fixip", "type": "object", "required": false, "nodes": [ 31 | {"key": "blacklistedObjectTypes", "type": "array", "required": true, "items": "string", "minCount": 0, "minLength": 1}, 32 | {"key": "unproperSubnets", "type": "array", "required": true, "items": "string", "minCount": 0, "minLength": 1} 33 | 34 | ]}, 35 | {"key": "log", "type": "object", "required": false, "nodes": [ 36 | {"key": "colorize", "type": "boolean", "required": true}, 37 | {"key": "verbosity", "type": "integer", "required": true, "ge": 1, "le": 63} 38 | ]} 39 | ] 40 | -------------------------------------------------------------------------------- /config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "server": [ 4 | { 5 | "category": "model", 6 | "attribute": "manufacturer" 7 | }, 8 | { 9 | "category": "model", 10 | "attribute": "model" 11 | }, 12 | { 13 | "category": "host address", 14 | "attribute": "ipv4_address" 15 | }, 16 | { 17 | "category": "host address", 18 | "attribute": "hostname" 19 | }, 20 | { 21 | "category": "host address", 22 | "attribute": "domain" 23 | }, 24 | { 25 | "category": "location", 26 | "attribute": "location", 27 | "default": "Root location" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /idoit.bash-completion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Copyright (C) 2016-18 Benjamin Heisig 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU Affero General Public License as published by 7 | ## the Free Software Foundation, either version 3 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU Affero General Public License for more details. 14 | ## 15 | ## You should have received a copy of the GNU Affero General Public License 16 | ## along with this program. If not, see . 17 | 18 | _idoit_command() { 19 | local cur options commands 20 | COMPREPLY=() 21 | cur="${COMP_WORDS[COMP_CWORD]}" 22 | prev="${COMP_WORDS[COMP_CWORD-1]}" 23 | options="-c -h --config --help --version" 24 | commands="init status read show search random nextip fixip help categories types" 25 | 26 | case "$prev" in 27 | read) 28 | local path=`idoit read $2 2> /dev/null | sed ':a;{N;s/\n/ /};ba'` 29 | COMPREPLY=( $( compgen -W "$path" -- $cur ) ) 30 | #local files=( $(idoit read $2 2> /dev/null) ) 31 | #local files=("/tmp/$2"*) 32 | #[[ -e ${files[0]} ]] && COMPREPLY=( "${files[@]##*/}" ) 33 | return 0 34 | ;; 35 | esac 36 | 37 | case "$cur" in 38 | -*) COMPREPLY=( $( compgen -W "${options}" -- $cur ) );; 39 | *) COMPREPLY=( $( compgen -W "${commands}" -- $cur ) );; 40 | esac 41 | 42 | return 0 43 | } 44 | 45 | complete -F _idoit_command idoit 46 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ../tests/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ../src/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Command/Cache.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \DirectoryIterator; 30 | use \Exception; 31 | 32 | /** 33 | * Command "cache" 34 | */ 35 | class Cache extends Command { 36 | 37 | /** 38 | * Execute command 39 | * 40 | * @return self Returns itself 41 | * 42 | * @throws Exception on error 43 | */ 44 | public function execute(): self { 45 | $this->log->info($this->getDescription()); 46 | 47 | $this 48 | ->clearCache() 49 | ->createCache(); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param string $file 56 | * @param mixed $value 57 | * 58 | * @return self Returns itself 59 | * 60 | * @throws Exception on error 61 | */ 62 | protected function serialize(string $file, $value): self { 63 | $filePath = $this->useCache()->getHostDir() . '/' . $file; 64 | 65 | $status = file_put_contents($filePath, serialize($value)); 66 | 67 | if ($status === false) { 68 | throw new Exception(sprintf( 69 | 'Unable to write to cache file "%s"', 70 | $filePath 71 | )); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @return self Returns itself 79 | * 80 | * @throws Exception on error 81 | */ 82 | protected function clearCache(): self { 83 | $hostDir = $this->useCache()->getHostDir(); 84 | 85 | if (!is_dir($hostDir)) { 86 | $this->log->info( 87 | 'Create cache directory for i-doit instance "%s"', 88 | $this->config['api']['url'] 89 | ); 90 | 91 | $status = mkdir($hostDir, 0775, true); 92 | 93 | if ($status === false) { 94 | throw new Exception(sprintf( 95 | 'Unable to create data directory "%s"', 96 | $hostDir 97 | )); 98 | } 99 | } else { 100 | $this->log->info('Clear cache files'); 101 | 102 | $files = new DirectoryIterator($hostDir); 103 | 104 | foreach ($files as $file) { 105 | if ($file->isFile()) { 106 | $status = @unlink($file->getPathname()); 107 | 108 | if ($status === false) { 109 | throw new Exception(sprintf( 110 | 'Unable to clear data in "%s". Unable to delete file "%s"', 111 | $hostDir, 112 | $file->getPathname() 113 | )); 114 | } 115 | } 116 | } 117 | } 118 | 119 | $this->log->debug( 120 | ' Stored cache files in directory %s', 121 | $hostDir 122 | ); 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @return self Returns itself 129 | * 130 | * @throws Exception on error 131 | */ 132 | protected function createCache(): self { 133 | $this->log->info('Fetch list of object types'); 134 | 135 | $objectTypes = $this->useIdoitAPIFactory()->getCMDBObjectTypes()->read(); 136 | 137 | switch (count($objectTypes)) { 138 | case 0: 139 | $this->log->notice( 140 | ' Found no object types' 141 | ); 142 | break; 143 | case 1: 144 | $this->log->debug( 145 | ' Found 1 object type' 146 | ); 147 | break; 148 | default: 149 | $this->log->debug( 150 | ' Found %s object types', 151 | count($objectTypes) 152 | ); 153 | break; 154 | } 155 | 156 | $this->serialize( 157 | 'object_types', 158 | $objectTypes 159 | ); 160 | 161 | $this->log->info('Fetch list of assigned categories'); 162 | 163 | $objectTypeIDs = []; 164 | 165 | foreach ($objectTypes as $objectType) { 166 | $objectTypeIDs[] = (int) $objectType['id']; 167 | } 168 | 169 | $batchedAssignedCategories = $this 170 | ->useIdoitAPIFactory() 171 | ->getCMDBObjectTypeCategories() 172 | ->batchReadByID($objectTypeIDs); 173 | 174 | for ($i = 0; $i < count($objectTypes); $i++) { 175 | switch (count($batchedAssignedCategories[$i])) { 176 | case 0: 177 | $this->log->notice( 178 | ' Object type "%s" [%s] has no categories assigned', 179 | $objectTypes[$i]['title'], 180 | $objectTypes[$i]['const'] 181 | ); 182 | break; 183 | case 1: 184 | $this->log->debug( 185 | ' Object type "%s" [%s] has 1 category assigned', 186 | $objectTypes[$i]['title'], 187 | $objectTypes[$i]['const'] 188 | ); 189 | break; 190 | default: 191 | $this->log->debug( 192 | ' Object type "%s" [%s] has %s categories assigned', 193 | $objectTypes[$i]['title'], 194 | $objectTypes[$i]['const'], 195 | count($batchedAssignedCategories[$i]) 196 | ); 197 | break; 198 | } 199 | 200 | $this->serialize( 201 | sprintf( 202 | 'object_type__%s', 203 | $objectTypes[$i]['const'] 204 | ), 205 | $batchedAssignedCategories[$i] 206 | ); 207 | } 208 | 209 | $this->log->info('Fetch information about categories'); 210 | 211 | $categoryConsts = []; 212 | 213 | $categories = []; 214 | 215 | $blacklistedCategories = $this 216 | ->useIdoitAPIFactory() 217 | ->getCMDBCategoryInfo() 218 | ->getVirtualCategoryConstants(); 219 | 220 | foreach ($batchedAssignedCategories as $assignedCategories) { 221 | foreach ($assignedCategories as $type => $categoryList) { 222 | foreach ($categoryList as $category) { 223 | if (in_array($category['const'], $blacklistedCategories)) { 224 | continue; 225 | } 226 | 227 | if (array_key_exists($category['const'], $categories)) { 228 | continue; 229 | } 230 | 231 | $categoryConsts[] = $category['const']; 232 | $categories[$category['const']] = $category; 233 | } 234 | } 235 | } 236 | 237 | switch (count($categoryConsts)) { 238 | case 0: 239 | $this->log->notice( 240 | ' Found no information about assigned categories' 241 | ); 242 | break; 243 | case 1: 244 | $this->log->debug( 245 | ' Found information about 1 category' 246 | ); 247 | break; 248 | default: 249 | $this->log->debug( 250 | ' Found information about %s categories', 251 | count($categoryConsts) 252 | ); 253 | break; 254 | } 255 | 256 | $propertyCounter = 0; 257 | 258 | if (count($categoryConsts) > 0) { 259 | $batchCategoryInfo = $this 260 | ->useIdoitAPIFactory() 261 | ->getCMDBCategoryInfo() 262 | ->batchRead($categoryConsts); 263 | 264 | $counter = 0; 265 | 266 | foreach ($categoryConsts as $categoryConst) { 267 | $categories[$categoryConst]['properties'] = $batchCategoryInfo[$counter]; 268 | 269 | $propertyCounter += count($batchCategoryInfo[$counter]); 270 | 271 | switch (count($batchCategoryInfo[$counter])) { 272 | case 0: 273 | $this->log->debug( 274 | ' Category "%s" [%s] has no properties', 275 | $categories[$categoryConst]['title'], 276 | $categoryConst 277 | ); 278 | break; 279 | case 1: 280 | $this->log->debug( 281 | ' Category "%s" [%s] has 1 property', 282 | $categories[$categoryConst]['title'], 283 | $categoryConst 284 | ); 285 | break; 286 | default: 287 | $this->log->debug( 288 | ' Category "%s" [%s] has %s properties', 289 | $categories[$categoryConst]['title'], 290 | $categoryConst, 291 | count($batchCategoryInfo[$counter]) 292 | ); 293 | break; 294 | } 295 | 296 | if (count($batchCategoryInfo[$counter]) > 0) { 297 | $this->serialize( 298 | sprintf( 299 | 'category__%s', 300 | $categoryConst 301 | ), 302 | $categories[$categoryConst] 303 | ); 304 | } 305 | 306 | $counter++; 307 | } 308 | } 309 | 310 | switch ($propertyCounter) { 311 | case 0: 312 | $this->log->notice( 313 | 'Found no properties over all categories' 314 | ); 315 | break; 316 | case 1: 317 | $this->log->debug( 318 | 'Found 1 property over all categories' 319 | ); 320 | break; 321 | default: 322 | $this->log->debug( 323 | 'Found %s properties over all categories', 324 | $propertyCounter 325 | ); 326 | break; 327 | } 328 | 329 | return $this; 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /src/Command/Call.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \bheisig\cli\IO; 31 | 32 | /** 33 | * Command "call" 34 | */ 35 | class Call extends Command { 36 | 37 | /** 38 | * Execute command 39 | * 40 | * @return self Returns itself 41 | * 42 | * @throws Exception on error 43 | */ 44 | public function execute(): self { 45 | $query = $this->getQuery(); 46 | 47 | if ($query === '') { 48 | while (true) { 49 | $line = IO::in(''); 50 | 51 | if ($line === '') { 52 | break; 53 | } 54 | 55 | $query .= $line; 56 | } 57 | } 58 | 59 | $request = json_decode(trim($query), true); 60 | 61 | if (!is_array($request)) { 62 | throw new Exception('No valid JSON input'); 63 | } 64 | 65 | if (!array_key_exists('method', $request)) { 66 | throw new Exception('No RPC method specified'); 67 | } 68 | 69 | $params = null; 70 | 71 | if (array_key_exists('params', $request)) { 72 | $params = $request['params']; 73 | } 74 | 75 | $this->useIdoitAPIFactory()->getAPI()->request($request['method'], $params); 76 | 77 | $requestContent = json_encode( 78 | $this->useIdoitAPIFactory()->getAPI()->getLastRequestContent(), 79 | JSON_PRETTY_PRINT 80 | ); 81 | 82 | if (!is_string($requestContent)) { 83 | $requestContent = '–'; 84 | } 85 | 86 | $responseContent = json_encode( 87 | $this->useIdoitAPIFactory()->getAPI()->getLastResponse(), 88 | JSON_PRETTY_PRINT 89 | ); 90 | 91 | if (!is_string($responseContent)) { 92 | $responseContent = '–'; 93 | } 94 | 95 | IO::out(''); 96 | IO::out('Request:'); 97 | IO::out('========'); 98 | IO::out($this->useIdoitAPIFactory()->getAPI()->getLastRequestHeaders()); 99 | IO::out($requestContent); 100 | IO::out(''); 101 | IO::out('Response:'); 102 | IO::out('========='); 103 | IO::out($this->useIdoitAPIFactory()->getAPI()->getLastResponseHeaders()); 104 | IO::out($responseContent); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Print usage of command 111 | * 112 | * @return self Returns itself 113 | */ 114 | public function printUsage(): self { 115 | $this->log->info( 116 | 'Usage: %1$s %2$s [OPTIONS] [REQUEST] 117 | 118 | %3$s 119 | 120 | REQUEST is a JSON-formatted string. Leave empty to read from standard input. 121 | 122 | Examples: 123 | 124 | 1) %1$s %2$s \\ 125 | \'{"version": "2.0","method": "idoit.version","params": {"apikey": "c1ia5q","language": "en"},"id": 1}\' 126 | 2) echo \\ 127 | \'{"version": "2.0","method": "idoit.version","params": {"apikey": "c1ia5q","language": "en"},"id": 1}\' \\ 128 | | %1$s %2$s 129 | 3) cat request.txt | %1$s %2$s', 130 | $this->config['composer']['extra']['name'], 131 | $this->getName(), 132 | $this->getDescription() 133 | ); 134 | 135 | return $this; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/Command/Categories.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | 31 | /** 32 | * Command "categories" 33 | */ 34 | class Categories extends Command { 35 | 36 | /** 37 | * Execute command 38 | * 39 | * @return self Returns itself 40 | * 41 | * @throws Exception on error 42 | */ 43 | public function execute(): self { 44 | $this->log 45 | ->printAsMessage() 46 | ->info($this->getDescription()) 47 | ->printEmptyLine(); 48 | 49 | $categories = $this->useCache()->getCategories(); 50 | 51 | $categories = $this->filterCategories($categories); 52 | 53 | $types = [ 54 | [ 55 | 'type' => 'CATG', 56 | 'title' => 'Global categories', 57 | 'arg' => 'global', 58 | 'active' => true 59 | ], 60 | [ 61 | 'type' => 'CATS', 62 | 'title' => 'Specific categories', 63 | 'arg' => 'specific', 64 | 'active' => true 65 | ] 66 | ]; 67 | 68 | $keepActivated = []; 69 | 70 | foreach ($types as $type) { 71 | if (in_array('--' . $type['arg'], $this->config['args'])) { 72 | $keepActivated[] = $type['type']; 73 | } 74 | } 75 | 76 | if (count($keepActivated) > 0) { 77 | for ($i = 0; $i < count($types); $i++) { 78 | if (!in_array($types[$i]['type'], $keepActivated)) { 79 | $types[$i]['active'] = false; 80 | } 81 | } 82 | } 83 | 84 | foreach ($types as $typeDetails) { 85 | if ($typeDetails['active'] === true) { 86 | // Type casting is necessary because phpstan unfortunately throws an error: 87 | $this->formatList( 88 | (string) $typeDetails['title'], 89 | (string) $typeDetails['type'], 90 | $categories 91 | ); 92 | 93 | $this->log 94 | ->printAsMessage() 95 | ->printEmptyLine(); 96 | } 97 | } 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * 104 | * @param array $categories 105 | * 106 | * @return array 107 | * 108 | * @throws Exception on error 109 | */ 110 | protected function filterCategories(array $categories): array { 111 | $result = []; 112 | 113 | if (in_array('--enabled', $this->config['args'])) { 114 | $enabledCategories = $this->getEnabledCategories(); 115 | 116 | foreach ($categories as $category) { 117 | if (in_array($category['const'], $enabledCategories)) { 118 | $result[] = $category; 119 | } 120 | } 121 | } elseif (in_array('--disabled', $this->config['args'])) { 122 | $enabledCategories = $this->getEnabledCategories(); 123 | 124 | foreach ($categories as $category) { 125 | if (!in_array($category['const'], $enabledCategories)) { 126 | $result[] = $category; 127 | } 128 | } 129 | } else { 130 | $result = $categories; 131 | } 132 | 133 | return $result; 134 | } 135 | 136 | /** 137 | * Get list of categories which are assigned to one or more object types 138 | * 139 | * @return array 140 | * 141 | * @throws Exception on error 142 | */ 143 | protected function getEnabledCategories(): array { 144 | $result = []; 145 | 146 | $objectTypes = $this->useCache()->getObjectTypes(); 147 | 148 | foreach ($objectTypes as $objectType) { 149 | $assignedCategories = $this->useCache()->getAssignedCategories($objectType['const']); 150 | 151 | foreach ($assignedCategories as $type => $categories) { 152 | foreach ($categories as $category) { 153 | $result[] = $category['const']; 154 | } 155 | } 156 | } 157 | 158 | return array_unique($result); 159 | } 160 | 161 | /** 162 | * 163 | * 164 | * @param string $title 165 | * @param string $type 166 | * @param array $categories 167 | */ 168 | protected function formatList(string $title, string $type, array $categories) { 169 | $this->log 170 | ->printAsMessage() 171 | ->info('%s [%s]:', $title, $type) 172 | ->printEmptyLine(); 173 | 174 | $categories = $this->filterByType($categories, $type); 175 | 176 | switch (count($categories)) { 177 | case 0: 178 | $this->log 179 | ->printAsMessage() 180 | ->info('No categories found'); 181 | break; 182 | case 1: 183 | $this->log 184 | ->printAsMessage() 185 | ->info('1 category found'); 186 | break; 187 | default: 188 | $this->log 189 | ->printAsMessage() 190 | ->info('%s categories found', count($categories)); 191 | break; 192 | } 193 | 194 | usort($categories, [$this, 'sortCategories']); 195 | 196 | foreach ($categories as $category) { 197 | $this->log 198 | ->printAsOutput() 199 | ->info($this->formatCategory($category)); 200 | } 201 | } 202 | 203 | /** 204 | * 205 | * 206 | * @param array $categories 207 | * @param string $type 208 | * 209 | * @return array 210 | */ 211 | protected function filterByType(array $categories, string $type): array { 212 | $result = []; 213 | 214 | foreach ($categories as $category) { 215 | $constPrefix = 'C__' . $type . '__'; 216 | if (strpos($category['const'], $constPrefix) === 0) { 217 | $result[] = $category; 218 | } 219 | } 220 | 221 | return $result; 222 | } 223 | 224 | /** 225 | * 226 | * 227 | * @param array $category 228 | * 229 | * @return string 230 | */ 231 | protected function formatCategory(array $category): string { 232 | return sprintf( 233 | '%s [%s]', 234 | $category['title'], 235 | $category['const'] 236 | ); 237 | } 238 | 239 | /** 240 | * 241 | * 242 | * @param array $a 243 | * @param array $b 244 | * 245 | * @return int 246 | */ 247 | protected function sortCategories(array $a, array $b): int { 248 | return strcmp($a['title'], $b['title']); 249 | } 250 | 251 | /** 252 | * Print usage of command 253 | * 254 | * @return self Returns itself 255 | */ 256 | public function printUsage(): self { 257 | $this->log->info( 258 | <<< EOF 259 | %3\$s 260 | 261 | USAGE 262 | \$ %1\$s %2\$s [OPTIONS] 263 | 264 | COMMAND OPTIONS 265 | --enabled Only list categories which are assigned to object 266 | types 267 | --disabled Only list categories which are not assigned to any 268 | object type 269 | 270 | --global Only list global categories 271 | --specific Only list specific categories 272 | 273 | COMMON OPTIONS 274 | -c FILE, Include settings stored in a JSON-formatted 275 | --config=FILE configuration file FILE; repeat option for more 276 | than one FILE 277 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 278 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 279 | repeat option for more than one KEY 280 | 281 | --no-colors Do not print colored messages 282 | -q, --quiet Do not output messages, only errors 283 | -v, --verbose Be more verbose 284 | 285 | -h, --help Print this help or information about a 286 | specific command 287 | --version Print version information 288 | 289 | -y, --yes No user interaction required; answer questions 290 | automatically with default values 291 | 292 | EXAMPLES 293 | # List all available categories: 294 | \$ %1\$s %2\$s 295 | 296 | # List global categories which are assigned to one or more object types: 297 | \$ %1\$s %2\$s --enabled --global 298 | EOF 299 | , 300 | $this->config['composer']['extra']['name'], 301 | $this->getName(), 302 | $this->getDescription() 303 | ); 304 | 305 | return $this; 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | use \RuntimeException; 32 | use bheisig\cli\Command\Command as BaseCommand; 33 | use bheisig\cli\Config; 34 | use bheisig\cli\JSONFile; 35 | use bheisig\idoitcli\Service\Cache; 36 | use bheisig\idoitcli\Service\HandleAttribute; 37 | use bheisig\idoitcli\Service\IdoitAPI; 38 | use bheisig\idoitcli\Service\IdoitAPIFactory; 39 | use bheisig\idoitcli\Service\PrintData; 40 | use bheisig\idoitcli\Service\Validate; 41 | 42 | /** 43 | * Base class for commands 44 | */ 45 | abstract class Command extends BaseCommand { 46 | 47 | /** 48 | * @var HandleAttribute 49 | */ 50 | protected $handleAttribute; 51 | 52 | /** 53 | * i-doit API 54 | * 55 | * @var IdoitAPI 56 | */ 57 | protected $idoitAPI; 58 | 59 | /** 60 | * i-doit API factory 61 | * 62 | * @var IdoitAPIFactory 63 | */ 64 | protected $idoitAPIFactory; 65 | 66 | /** 67 | * @var Cache 68 | */ 69 | protected $cache; 70 | 71 | /** 72 | * @var PrintData 73 | */ 74 | protected $printData; 75 | 76 | /** 77 | * @var Validate 78 | */ 79 | protected $validate; 80 | 81 | /** 82 | * Process some routines before executing command 83 | * 84 | * @return self Returns itself 85 | * 86 | * @throws Exception on error 87 | */ 88 | public function setup(): self { 89 | parent::setup(); 90 | 91 | $this->validateConfig(); 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Load service for user interaction 98 | * 99 | * @return Cache 100 | * 101 | * @throws Exception when cache is out-dated 102 | */ 103 | protected function useCache(): Cache { 104 | if (!isset($this->cache)) { 105 | $this->cache = new Cache($this->config, $this->log); 106 | } 107 | 108 | if ($this->cache->isCached() === false && 109 | $this->config['command'] !== 'cache') { 110 | throw new RuntimeException(sprintf( 111 | 'Unsufficient data. Please run "%s cache" first.', 112 | $this->config['composer']['extra']['name'] 113 | ), 500); 114 | } 115 | 116 | return $this->cache; 117 | } 118 | 119 | /** 120 | * Load service for attribute handling 121 | * 122 | * @return HandleAttribute 123 | * 124 | * @throws Exception on error 125 | */ 126 | protected function handleAttribute(): HandleAttribute { 127 | if (!isset($this->handleAttribute)) { 128 | $this->handleAttribute = (new HandleAttribute($this->config, $this->log)) 129 | ->setUp($this->useIdoitAPI(), $this->useIdoitAPIFactory()); 130 | } 131 | 132 | return $this->handleAttribute; 133 | } 134 | 135 | /** 136 | * Load service for i-doit API 137 | * 138 | * @return IdoitAPI 139 | * 140 | * @throws Exception on error 141 | */ 142 | protected function useIdoitAPI(): IdoitAPI { 143 | if (!isset($this->idoitAPI)) { 144 | $this->idoitAPI = new IdoitAPI($this->config, $this->log); 145 | $this->idoitAPI->setUp($this->useIdoitAPIFactory()); 146 | } 147 | 148 | return $this->idoitAPI; 149 | } 150 | 151 | /** 152 | * Load service for i-doit API factory 153 | * 154 | * @return IdoitAPIFactory 155 | * 156 | * @throws Exception on error 157 | */ 158 | protected function useIdoitAPIFactory(): IdoitAPIFactory { 159 | if (!isset($this->idoitAPIFactory)) { 160 | $this->idoitAPIFactory = new IdoitAPIFactory($this->config, $this->log); 161 | } 162 | 163 | return $this->idoitAPIFactory; 164 | } 165 | 166 | /** 167 | * Load service for attribute handling 168 | * 169 | * @return PrintData 170 | * 171 | * @throws Exception on error 172 | */ 173 | protected function printData(): PrintData { 174 | if (!isset($this->printData)) { 175 | $this->printData = new PrintData($this->config, $this->log); 176 | } 177 | 178 | return $this->printData; 179 | } 180 | 181 | /** 182 | * Load service for validate data 183 | * 184 | * @return Validate 185 | */ 186 | protected function useValidate(): Validate { 187 | if (!isset($this->validate)) { 188 | $this->validate = new Validate($this->config, $this->log); 189 | } 190 | 191 | return $this->validate; 192 | } 193 | 194 | /** 195 | * Print debug information 196 | * 197 | * @param array $arr 198 | * 199 | * @param int $nested 200 | */ 201 | protected function printDebug(array $arr, int $nested = 0) { 202 | if (count($arr) === 0) { 203 | $this->log->debug( 204 | '%s–', 205 | str_repeat(' ', $nested * 4) 206 | ); 207 | 208 | return; 209 | } 210 | 211 | foreach ($arr as $key => $value) { 212 | switch (gettype($value)) { 213 | case 'array': 214 | $this->log->debug( 215 | '%s%s:', 216 | str_repeat(' ', $nested * 4), 217 | $key 218 | ); 219 | 220 | $this->printDebug($value, ($nested + 1)); 221 | break; 222 | default: 223 | $this->log->debug( 224 | '%s%s: %s', 225 | str_repeat(' ', $nested * 4), 226 | $key, 227 | $value 228 | ); 229 | break; 230 | } 231 | } 232 | } 233 | 234 | /** 235 | * @return array Object 236 | * @throws Exception on error 237 | */ 238 | protected function identifyObjectByArgument(): array { 239 | switch (count($this->config['arguments'])) { 240 | case 0: 241 | if ($this->useUserInteraction()->isInteractive() === false) { 242 | throw new BadMethodCallException( 243 | 'No object, no visuals' 244 | ); 245 | } 246 | 247 | return $this->askForObject(); 248 | case 1: 249 | return $this->useIdoitAPI()->identifyObject( 250 | $this->config['arguments'][0] 251 | ); 252 | default: 253 | throw new BadMethodCallException( 254 | 'Too many arguments; please provide only one object title or numeric identifier' 255 | ); 256 | } 257 | } 258 | 259 | /** 260 | * Ask user for object title or numeric identifier 261 | * 262 | * …until object has been identified 263 | * 264 | * @return array Object information, otherwise \Exception is thrown 265 | * 266 | * @throws Exception on error 267 | */ 268 | protected function askForObject(): array { 269 | $answer = $this->useUserInteraction()->askQuestion('Object?'); 270 | 271 | if (strlen($answer) === 0) { 272 | $this->log->warning('Please re-try'); 273 | return $this->askForObject(); 274 | } 275 | 276 | try { 277 | return $this->useIdoitAPI()->identifyObject($answer); 278 | } catch (BadMethodCallException $e) { 279 | $this->log->warning($e->getMessage()); 280 | $this->log->warning('Please re-try'); 281 | return $this->askForObject(); 282 | } 283 | } 284 | 285 | /** 286 | * Validate configuration settings 287 | * 288 | * @return self Returns itself 289 | * 290 | * @throws Exception on error 291 | */ 292 | protected function validateConfig(): self { 293 | $file = $this->config['appDir'] . '/config/schema.json'; 294 | $rules = JSONFile::read($file); 295 | $config = new Config(); 296 | $errors = $config->validate($this->config, $rules); 297 | 298 | if (count($errors) > 0) { 299 | $this->log->warning('One or more errors found in configuration settings:'); 300 | 301 | foreach ($errors as $error) { 302 | $this->log->warning($error); 303 | } 304 | 305 | throw new Exception('Cannot proceed unless you fix your configuration'); 306 | } 307 | 308 | return $this; 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /src/Command/Create.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | 31 | /** 32 | * Command "create" 33 | */ 34 | class Create extends Command { 35 | 36 | /** 37 | * Path 38 | * 39 | * @var array Indexed array 40 | */ 41 | protected $path = []; 42 | 43 | /** 44 | * Object type constant in path 45 | * 46 | * @var string 47 | */ 48 | protected $objectTypeConst; 49 | 50 | /** 51 | * Processes some routines before the execution 52 | * 53 | * @return self Returns itself 54 | * 55 | * @throws Exception on error 56 | */ 57 | public function setup(): Command { 58 | parent::setup(); 59 | 60 | $this->path = explode('/', $this->getQuery()); 61 | 62 | $objectTypes = $this->useCache()->getObjectTypes(); 63 | 64 | foreach ($objectTypes as $objectType) { 65 | if (strtolower($objectType['title']) === strtolower($this->path[0])) { 66 | $this->objectTypeConst = $objectType['const']; 67 | break; 68 | } 69 | } 70 | 71 | $this->log->warning('This command is deprecated. Please use command "save" instead.'); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Execute command 78 | * 79 | * @return self Returns itself 80 | * 81 | * @throws Exception on error 82 | */ 83 | public function execute(): self { 84 | switch (count($this->path)) { 85 | case 1: 86 | throw new Exception('Path has only 1 part'); 87 | case 2: 88 | if (isset($this->objectTypeConst)) { 89 | $objectID = $this->useIdoitAPIFactory()->getCMDBObject()->create( 90 | $this->objectTypeConst, 91 | $this->path[1] 92 | ); 93 | 94 | $this->log->info('Object with ID %s successfully created', $objectID); 95 | } else { 96 | $objects = $this->useIdoitAPI()->fetchObjects(['title' => $this->path[0]]); 97 | 98 | $this->createCategoryEntries($objects, $this->getCategoryConst($this->path[1])); 99 | } 100 | break; 101 | case 3: 102 | $objects = $this->useIdoitAPI()->fetchObjects([ 103 | 'type' => $this->objectTypeConst, 104 | 'title' => $this->path[1] 105 | ]); 106 | 107 | $this->createCategoryEntries($objects, $this->getCategoryConst($this->path[2])); 108 | break; 109 | default: 110 | throw new Exception('Path has too many parts'); 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * 118 | * 119 | * @param array $objects 120 | * @param string $category 121 | * 122 | * @throws Exception on error 123 | */ 124 | protected function createCategoryEntries(array $objects, string $category) { 125 | switch (count($objects)) { 126 | case 0: 127 | throw new Exception('No object found'); 128 | } 129 | 130 | $attributes = $this->getAttributes(); 131 | 132 | $objectIDs = array_map(function ($object) { 133 | return (int) $object['id']; 134 | }, $objects); 135 | 136 | $this->useIdoitAPIFactory()->getCMDBCategory()->batchCreate( 137 | $objectIDs, 138 | $category, 139 | [$attributes] 140 | ); 141 | 142 | switch (count($objects)) { 143 | case 1: 144 | $this->log->info('Created 1 entry'); 145 | break; 146 | default: 147 | $this->log->info('Created %s entries', count($objects)); 148 | break; 149 | } 150 | } 151 | 152 | /** 153 | * 154 | * 155 | * @param string $category 156 | * 157 | * @return string 158 | * 159 | * @throws Exception on error 160 | */ 161 | protected function getCategoryConst(string $category): string { 162 | $categories = $this->useCache()->getCategories(); 163 | 164 | $candidates = []; 165 | 166 | foreach ($categories as $categoryInfo) { 167 | if (strtolower($categoryInfo['title']) === strtolower($category)) { 168 | $candidates[] = $categoryInfo; 169 | } 170 | 171 | if (strtolower($categoryInfo['const']) === strtolower($category)) { 172 | return $category; 173 | } 174 | } 175 | 176 | switch (count($candidates)) { 177 | case 0: 178 | throw new Exception(sprintf( 179 | 'Unknown category "%s"', 180 | $category 181 | )); 182 | case 1: 183 | return $candidates[0]['const']; 184 | default: 185 | $this->log->warning('Unambigious category title:'); 186 | 187 | foreach ($candidates as $candidate) { 188 | $this->log->warning( 189 | ' "%s" [%s]', 190 | $candidate['title'], 191 | $candidate['const'] 192 | ); 193 | } 194 | 195 | throw new Exception('Unable to create one or more category entries'); 196 | } 197 | } 198 | 199 | /** 200 | * @todo This argument parsing is pretty ugly. 201 | * 202 | * @return array 203 | */ 204 | protected function getAttributes(): array { 205 | $attributes = []; 206 | 207 | $pathKey = array_search($this->getQuery(), $this->config['args']); 208 | $options = array_slice( 209 | $this->config['args'], 210 | ++$pathKey 211 | ); 212 | 213 | for ($i = 0; $i < count($options); $i+=2) { 214 | $attribute = substr($options[$i], 2); 215 | 216 | if (array_key_exists(($i+1), $options)) { 217 | $value = $options[($i + 1)]; 218 | $attributes[$attribute] = $value; 219 | } 220 | } 221 | 222 | return $attributes; 223 | } 224 | 225 | /** 226 | * Print usage of command 227 | * 228 | * @return self Returns itself 229 | */ 230 | public function printUsage(): self { 231 | $this->log->info( 232 | 'Usage: %1$s %2$s [OPTIONS] PATH 233 | 234 | %3$s 235 | 236 | Add new server "host.example.net": 237 | 238 | %1$s %2$s server/host.example.net 239 | 240 | Add model for server "host.example.net": 241 | 242 | %1$s %2$s server/host.example.net/model --manufacturer VendorXY --title Model123 243 | %1$s %2$s host.example.net/model --manufacturer VendorXY --title Model123 244 | 245 | Add model to all servers: 246 | 247 | %1$s %2$s server/*/model --manufacturer VendorXY --title Model123', 248 | $this->config['composer']['extra']['name'], 249 | $this->getName(), 250 | $this->getDescription() 251 | ); 252 | 253 | return $this; 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /src/Command/Dummy.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-19 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | 31 | /** 32 | * Command "dummy" 33 | */ 34 | class Dummy extends Command { 35 | 36 | /** 37 | * Execute command 38 | * 39 | * @return self Returns itself 40 | * 41 | * @throws Exception on error 42 | */ 43 | public function execute(): self { 44 | $this->log 45 | ->printAsMessage() 46 | ->info('Hello, world!') 47 | ->printEmptyLine(); 48 | 49 | return $this; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/Init.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use bheisig\cli\Command\Init as BaseInit; 31 | 32 | /** 33 | * Command "init" 34 | */ 35 | class Init extends Command { 36 | 37 | /** 38 | * Process some routines before executing command 39 | * 40 | * @return self Returns itself 41 | * 42 | * @throws Exception on error 43 | */ 44 | public function setup(): Command { 45 | // Restore basic setup to skip validation of configuration settings: 46 | $this->start = time(); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Execute command 53 | * 54 | * @return self Returns itself 55 | * 56 | * @throws Exception on error 57 | */ 58 | public function execute(): self { 59 | $this->log 60 | ->printAsMessage() 61 | ->info($this->getDescription()) 62 | ->printEmptyLine() 63 | ->printAsOutput(); 64 | 65 | try { 66 | $baseInit = new BaseInit($this->config, $this->log); 67 | $baseInit->execute(); 68 | 69 | $this->log 70 | ->info('Create cache files needed for faster processing:') 71 | ->printEmptyLine() 72 | ->info( 73 | ' %s cache', 74 | $this->config['composer']['extra']['name'] 75 | ); 76 | } catch (Exception $e) { 77 | $code = $e->getCode(); 78 | 79 | if ($code === 0) { 80 | $code = 500; 81 | } 82 | 83 | throw new Exception( 84 | sprintf( 85 | 'Unable to initiate "%s": ' . $e->getMessage(), 86 | $this->config['composer']['extra']['name'] 87 | ), 88 | $code 89 | ); 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Command/Log.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | use \RuntimeException; 32 | 33 | /** 34 | * Command "log" 35 | */ 36 | class Log extends Command { 37 | 38 | protected $objectID; 39 | protected $objectTitle; 40 | protected $message; 41 | 42 | /** 43 | * Execute command 44 | * 45 | * @return self Returns itself 46 | * 47 | * @throws Exception on error 48 | */ 49 | public function execute(): self { 50 | $this->log 51 | ->printAsMessage() 52 | ->info($this->getDescription()) 53 | ->printEmptyLine(); 54 | 55 | switch (count($this->config['arguments'])) { 56 | case 0: 57 | break; 58 | case 1: 59 | $object = $this->useIdoitAPI()->identifyObject( 60 | $this->config['arguments'][0] 61 | ); 62 | 63 | $this->objectID = (int) $object['id']; 64 | $this->objectTitle = $object['title']; 65 | break; 66 | default: 67 | throw new BadMethodCallException( 68 | 'Too many arguments; please provide only one object title or numeric identifier' 69 | ); 70 | } 71 | 72 | $this->identifyMessage($this->config['options']); 73 | 74 | if (!$this->hasObject()) { 75 | if ($this->useUserInteraction()->isInteractive() === false) { 76 | throw new BadMethodCallException( 77 | 'No object, no log' 78 | ); 79 | } 80 | 81 | $object = $this->askForObject(); 82 | $this->objectID = (int) $object['id']; 83 | $this->objectTitle = $object['title']; 84 | } 85 | 86 | $this->reportObject(); 87 | 88 | if (!$this->hasMessage()) { 89 | if ($this->useUserInteraction()->isInteractive() === false) { 90 | throw new BadMethodCallException( 91 | 'No message, no log' 92 | ); 93 | } 94 | 95 | $this->askForMessage(); 96 | } 97 | 98 | $this->reportMessage(); 99 | 100 | $this->save(); 101 | 102 | return $this; 103 | } 104 | 105 | protected function identifyMessage(array $options): self { 106 | $message = ''; 107 | 108 | if (array_key_exists('m', $options)) { 109 | if (is_array($options['m'])) { 110 | throw new BadMethodCallException( 111 | 'Use option -m only once' 112 | ); 113 | } 114 | 115 | $message = $options['m']; 116 | } 117 | 118 | if (array_key_exists('message', $options)) { 119 | if (isset($message)) { 120 | throw new BadMethodCallException( 121 | 'Use either option -m or --message, not both' 122 | ); 123 | } 124 | 125 | if (is_array($options['message'])) { 126 | throw new BadMethodCallException( 127 | 'Use option --message only once' 128 | ); 129 | } 130 | 131 | $message = $options['message']; 132 | } 133 | 134 | if (strlen($message) > 0) { 135 | $this->message = $message; 136 | } 137 | 138 | return $this; 139 | } 140 | 141 | protected function hasObject(): bool { 142 | return isset($this->objectID); 143 | } 144 | 145 | protected function hasMessage(): bool { 146 | return isset($this->message); 147 | } 148 | 149 | /** 150 | * @return self Returns itself 151 | * 152 | * @throws Exception on error 153 | */ 154 | protected function askForMessage(): self { 155 | try { 156 | if (function_exists('exec') === false) { 157 | throw new RuntimeException( 158 | 'Executing external commands via shell is not allowed; probably function "exec" is disabled' 159 | ); 160 | } 161 | 162 | $editor = $this->findEditor(); 163 | 164 | if ($editor === '') { 165 | // Last chance: 166 | $editor = $this->askForEditor(); 167 | } 168 | 169 | $this->log->debug('Editor: %s', $editor); 170 | 171 | $tmpFile = $temp_file = tempnam( 172 | sys_get_temp_dir(), 173 | $this->config['composer']['extra']['name'] . '_' 174 | ); 175 | 176 | if (!is_string($tmpFile) || !is_writable($tmpFile)) { 177 | throw new RuntimeException(sprintf( 178 | 'Permission denied to write file "%s"', 179 | $tmpFile 180 | )); 181 | } 182 | 183 | $objectTitle = $this->objectTitle; 184 | $objectID = $this->objectID; 185 | 186 | $status = file_put_contents( 187 | $tmpFile, 188 | << `tty`", $output, $exitCode); 209 | 210 | if ($exitCode > 0) { 211 | foreach ($output as $line) { 212 | $this->log->warning($line); 213 | } 214 | 215 | throw new RuntimeException(sprintf( 216 | 'Editor closed with exit code %s', 217 | $exitCode 218 | )); 219 | } 220 | 221 | $content = file_get_contents($tmpFile); 222 | 223 | if ($content === false) { 224 | throw new RuntimeException(sprintf( 225 | 'Unable to read from temporary file "%s"', 226 | $tmpFile 227 | )); 228 | } 229 | 230 | $status = unlink($tmpFile); 231 | 232 | if ($status === false) { 233 | throw new RuntimeException(sprintf( 234 | 'Unable to remove temporary file "%s"', 235 | $tmpFile 236 | )); 237 | } 238 | 239 | $lines = explode(PHP_EOL, $content); 240 | 241 | $messageLines = []; 242 | 243 | foreach ($lines as $line) { 244 | if (strpos($line, '#') === 0) { 245 | continue; 246 | } 247 | 248 | $messageLines[] = $line; 249 | } 250 | 251 | $this->message = trim(implode(PHP_EOL, $messageLines)); 252 | 253 | if (strlen($this->message) === 0) { 254 | throw new RuntimeException( 255 | 'Your message is empty' 256 | ); 257 | } 258 | } catch (Exception $e) { 259 | throw new RuntimeException(sprintf( 260 | 'Asking for a message resulted in an error: %s', 261 | $e->getMessage() 262 | )); 263 | } 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Find a proper editor to edit files 270 | * 271 | * Try (in the following order): 272 | * - Environment variable "EDITOR" 273 | * - Environment variable "VISUAL" 274 | * - Command "editor" 275 | * - Command "sensible-editor" 276 | * - Command "xdg-mime query default" 277 | * - Editor "nano" 278 | * - Editor "vim" 279 | * - Editor "vi" 280 | * - Editor "joe" 281 | * - Editor "gedit" 282 | * - Editor "kate" 283 | * 284 | * @return string 285 | */ 286 | protected function findEditor(): string { 287 | $environmentVariables = [ 288 | 'EDITOR', 289 | 'VISUAL' 290 | ]; 291 | 292 | foreach ($environmentVariables as $environmentVariable) { 293 | $this->log->debug('Check environment variable "%s"', $environmentVariable); 294 | 295 | $editor = getenv($environmentVariable); 296 | 297 | if ($editor !== false && strlen($editor) > 0) { 298 | return $editor; 299 | } 300 | } 301 | 302 | $commands = [ 303 | 'editor', 304 | 'sensible-editor', 305 | 'xdg-mime query default', 306 | 'nano', 307 | 'vim', 308 | 'vi', 309 | 'joe', 310 | 'gedit', 311 | 'kate' 312 | ]; 313 | 314 | foreach ($commands as $command) { 315 | $this->log->debug('Check command "%s"', $command); 316 | 317 | if ($this->testCommand($command)) { 318 | return $command; 319 | } 320 | } 321 | 322 | $this->log->debug('No editor found'); 323 | 324 | return ''; 325 | } 326 | 327 | protected function testCommand(string $command): bool { 328 | $output = []; 329 | $exitCode = 0; 330 | 331 | exec("type $command > /dev/null 2> /dev/null", $output, $exitCode); 332 | 333 | if ($exitCode === 0) { 334 | return true; 335 | } 336 | 337 | return false; 338 | } 339 | 340 | /** 341 | * @return string 342 | */ 343 | protected function askForEditor(): string { 344 | $editor = $this->useUserInteraction()->askQuestion('Which editor do you prefer?'); 345 | 346 | if (strlen($editor) === 0) { 347 | $this->log->warning('Excuse me, what do you mean?'); 348 | 349 | return $this->askForEditor(); 350 | } 351 | 352 | if ($this->testCommand($editor) === false) { 353 | $this->log->warning('Command not found'); 354 | 355 | return $this->askForEditor(); 356 | } 357 | 358 | return $editor; 359 | } 360 | 361 | protected function reportObject(): self { 362 | $this->log->debug( 363 | 'Object identified: %s [%s]', 364 | $this->objectTitle, 365 | $this->objectID 366 | ); 367 | 368 | return $this; 369 | } 370 | 371 | protected function reportMessage(): self { 372 | $this->log->debug( 373 | 'Message:', 374 | $this->message 375 | ); 376 | 377 | $lines = explode(PHP_EOL, $this->message); 378 | 379 | foreach ($lines as $line) { 380 | $this->log->debug($line); 381 | } 382 | 383 | return $this; 384 | } 385 | 386 | /** 387 | * @return self Returns itself 388 | * 389 | * @throws Exception on error 390 | */ 391 | protected function save(): self { 392 | $this->log->debug('Save…'); 393 | 394 | $this->useIdoitAPIFactory()->getCMDBLogbook()->create( 395 | $this->objectID, 396 | $this->message 397 | ); 398 | 399 | return $this; 400 | } 401 | 402 | /** 403 | * Print usage of command 404 | * 405 | * @return self Returns itself 406 | */ 407 | public function printUsage(): self { 408 | $editor = $this->findEditor(); 409 | 410 | if ($editor === '') { 411 | $editor = 'no editor found'; 412 | } 413 | 414 | $this->log->info( 415 | <<< EOF 416 | %3\$s 417 | 418 | USAGE 419 | \$ %1\$s %2\$s [OPTIONS] [OBJECT] 420 | 421 | ARGUMENTS 422 | OBJECT Object title or numeric identifier 423 | 424 | COMMAND OPTIONS 425 | -m MESSAGE, Add message text, otherwise type your message in 426 | --message=MESSAGE your prefered editor: %4\$s 427 | 428 | COMMON OPTIONS 429 | -c FILE, Include settings stored in a JSON-formatted 430 | --config=FILE configuration file FILE; repeat option for more 431 | than one FILE 432 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 433 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 434 | repeat option for more than one KEY 435 | 436 | --no-colors Do not print colored messages 437 | -q, --quiet Do not output messages, only errors 438 | -v, --verbose Be more verbose 439 | 440 | -h, --help Print this help or information about a 441 | specific command 442 | --version Print version information 443 | 444 | -y, --yes No user interaction required; answer questions 445 | automatically with default values 446 | 447 | EXAMPLES 448 | # Add entry to object identified by its title: 449 | \$ %1\$s %2\$s host01.example.com -m "Reboot server for Kernel updates" 450 | 451 | # …or by its numeric identifier: 452 | \$ %1\$s %2\$s 42 -m "Reboot server for Kernel updates" 453 | 454 | # If argument OBJECT or message is omitted you'll be asked for it: 455 | \$ %1\$s %2\$s 456 | Object? host01.example.com 457 | # Based on environment variable EDITOR 458 | # type your message in your prefered editor… 459 | EOF 460 | , 461 | $this->config['composer']['extra']['name'], 462 | $this->getName(), 463 | $this->getDescription(), 464 | $editor 465 | ); 466 | 467 | return $this; 468 | } 469 | 470 | } 471 | -------------------------------------------------------------------------------- /src/Command/Logs.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | 32 | /** 33 | * Command "logs" 34 | */ 35 | class Logs extends Command { 36 | 37 | /** 38 | * @var array 39 | */ 40 | protected $filterByIDs = []; 41 | 42 | /** 43 | * @var array 44 | */ 45 | protected $filterByTitles = []; 46 | 47 | /** 48 | * @var string 49 | */ 50 | protected $filterByDateTime = ''; 51 | 52 | /** 53 | * @var int 54 | */ 55 | protected $limit = 0; 56 | 57 | /** 58 | * @var int 59 | */ 60 | protected $hardLimit = 10000; 61 | 62 | /** 63 | * @var array 64 | */ 65 | protected $events = [ 66 | 'C__LOGBOOK_EVENT__OBJECTTYPE_PURGED' => 'Object type purged', 67 | 'C__LOGBOOK_EVENT__OBJECTTYPE_ARCHIVED' => 'Object type archived', 68 | 'C__LOGBOOK_EVENT__OBJECTTYPE_RECYCLED' => 'Object type recycled', 69 | 'C__LOGBOOK_EVENT__OBJECTTYPE_CHANGED' => 'Object type updated', 70 | 'C__LOGBOOK_EVENT__OBJECTTYPE_CREATED' => 'Object type created', 71 | 'C__LOGBOOK_EVENT__OBJECT_PURGED' => 'Object purged', 72 | 'C__LOGBOOK_EVENT__OBJECT_ARCHIVED' => 'Object archived', 73 | 'C__LOGBOOK_EVENT__OBJECT_RECYCLED' => 'Object recycled', 74 | 'C__LOGBOOK_EVENT__OBJECT_CHANGED' => 'Object updated', 75 | 'C__LOGBOOK_EVENT__OBJECT_CREATED' => 'Object created', 76 | 'C__LOGBOOK_EVENT__OBJECTTYPE_PURGED__NOT' => 'Object type purged… not', 77 | 'C__LOGBOOK_EVENT__OBJECTTYPE_ARCHIVED__NOT' => 'Object type archived… not', 78 | 'C__LOGBOOK_EVENT__OBJECTTYPE_RECYCLED__NOT' => 'Object type recycled… not', 79 | 'C__LOGBOOK_EVENT__OBJECTTYPE_CHANGED__NOT' => 'Object type updated… not', 80 | 'C__LOGBOOK_EVENT__OBJECTTYPE_CREATED__NOT' => 'Object type created… not', 81 | 'C__LOGBOOK_EVENT__OBJECT_PURGED__NOT' => 'Object purged… not', 82 | 'C__LOGBOOK_EVENT__OBJECT_ARCHIVED__NOT' => 'Object archived… not', 83 | 'C__LOGBOOK_EVENT__OBJECT_RECYCLED__NOT' => 'Object recycled… not', 84 | 'C__LOGBOOK_EVENT__OBJECT_CHANGED__NOT' => 'Object updated… not', 85 | 'C__LOGBOOK_EVENT__OBJECT_CREATED__NOT' => 'Object created… not', 86 | // @todo What are these events? 87 | // 'C__LOGBOOK_EVENT__POBJECT_FEMALE_SOCKET_CREATED__NOT' => '', 88 | // 'C__LOGBOOK_EVENT__POBJECT_MALE_PLUG_CREATED__NOT' => '', 89 | 'C__LOGBOOK_EVENT__CATEGORY_PURGED' => 'Category entry purged', 90 | 'C__LOGBOOK_EVENT__CATEGORY_CHANGED' => 'Category entry updated', 91 | 'C__LOGBOOK_EVENT__CATEGORY_ARCHIVED' => 'Category entry archvied', 92 | 'C__LOGBOOK_EVENT__CATEGORY_DELETED' => 'Category entry deleted' 93 | ]; 94 | 95 | /** 96 | * @var array 97 | */ 98 | protected $logs = []; 99 | 100 | /** 101 | * Execute command 102 | * 103 | * @return self Returns itself 104 | * 105 | * @throws Exception on error 106 | */ 107 | public function execute(): self { 108 | $this->log 109 | ->printAsMessage() 110 | ->info($this->getDescription()) 111 | ->printEmptyLine(); 112 | 113 | $this 114 | ->buildFilters($this->config['options']) 115 | ->fetchObjectIDsByTitles() 116 | ->readLogs() 117 | ->printLogs(); 118 | 119 | if ($this->follow()) { 120 | $this->keepFollowing(); 121 | } 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @param array $options 128 | * 129 | * @return self Returns itself 130 | * 131 | * @throws Exception on error 132 | */ 133 | protected function buildFilters(array $options): self { 134 | return $this 135 | ->buildIDFilter($options) 136 | ->buildTitleFilter($options) 137 | ->buildLimitFilter($options) 138 | ->buildDateTimeFilter($options); 139 | } 140 | 141 | /** 142 | * @param array $options 143 | * 144 | * @return self Returns itself 145 | * 146 | * @throws Exception on error 147 | */ 148 | protected function buildIDFilter(array $options): self { 149 | if (!array_key_exists('id', $options)) { 150 | return $this; 151 | } 152 | 153 | $ids = []; 154 | 155 | if (is_array($options['id'])) { 156 | $ids = $options['id']; 157 | } elseif (is_string($options['id'])) { 158 | $ids[] = $options['id']; 159 | } 160 | 161 | foreach ($ids as $id) { 162 | if ($this->useValidate()->isIDAsString($id) === false) { 163 | throw new BadMethodCallException(sprintf( 164 | 'Invalid filter by id: %s', 165 | $id 166 | )); 167 | } 168 | 169 | $this->filterByIDs[] = (int) $id; 170 | } 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * @param array $options 177 | * 178 | * @return self Returns itself 179 | * 180 | * @throws Exception on error 181 | */ 182 | protected function buildTitleFilter(array $options): self { 183 | if (!array_key_exists('title', $options)) { 184 | return $this; 185 | } 186 | 187 | $titles = []; 188 | 189 | if (is_array($options['title'])) { 190 | $titles = $options['title']; 191 | } elseif (is_string($options['title'])) { 192 | $titles[] = $options['title']; 193 | } 194 | 195 | foreach ($titles as $title) { 196 | if ($this->useValidate()->isOneLiner($title) === false) { 197 | throw new BadMethodCallException(sprintf( 198 | 'Invalid filter by title: %s', 199 | $title 200 | )); 201 | } 202 | 203 | $this->filterByTitles[] = $title; 204 | } 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @param array $options 211 | * 212 | * @return self Returns itself 213 | * 214 | * @throws Exception on error 215 | */ 216 | protected function buildLimitFilter(array $options): self { 217 | if (array_key_exists('n', $options)) { 218 | if (is_array($options['n'])) { 219 | throw new BadMethodCallException( 220 | 'Use option -n only once' 221 | ); 222 | } 223 | 224 | if (!is_numeric($options['n']) || 225 | (int) $options['n'] <= 0) { 226 | throw new BadMethodCallException( 227 | 'Option -n is not a positive integer' 228 | ); 229 | } 230 | 231 | $this->limit = (int) $options['n']; 232 | } 233 | 234 | if (array_key_exists('number', $options)) { 235 | if ($this->limit > 0) { 236 | throw new BadMethodCallException( 237 | 'Use either option -n or --number, not both' 238 | ); 239 | } 240 | 241 | if (is_array($options['number'])) { 242 | throw new BadMethodCallException( 243 | 'Use option --number only once' 244 | ); 245 | } 246 | 247 | if (!is_numeric($options['number']) || 248 | (int) $options['number'] <= 0) { 249 | throw new BadMethodCallException( 250 | 'Option --number is not a positive integer' 251 | ); 252 | } 253 | 254 | $this->limit = (int) $options['number']; 255 | } 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * @param array $options 262 | * 263 | * @return self Returns itself 264 | * 265 | * @throws Exception on error 266 | */ 267 | protected function buildDateTimeFilter(array $options): self { 268 | if (!array_key_exists('since', $options)) { 269 | return $this; 270 | } 271 | 272 | if (is_array($options['since'])) { 273 | throw new BadMethodCallException( 274 | 'Use option --since only once' 275 | ); 276 | } 277 | 278 | if (strtotime($options['since']) === false) { 279 | throw new BadMethodCallException( 280 | 'Option --since is not a valid date/time' 281 | ); 282 | } 283 | 284 | $this->filterByDateTime = $options['since']; 285 | 286 | return $this; 287 | } 288 | 289 | protected function hasIDFilter(): bool { 290 | return count($this->filterByIDs) > 0; 291 | } 292 | 293 | protected function hasTitleFilter(): bool { 294 | return count($this->filterByTitles) > 0; 295 | } 296 | 297 | protected function hasDateTimeFilter(): bool { 298 | return strlen($this->filterByDateTime) > 0; 299 | } 300 | 301 | protected function hasAnyFilter(): bool { 302 | return $this->hasIDFilter() || 303 | $this->hasTitleFilter() || 304 | $this->hasDateTimeFilter(); 305 | } 306 | 307 | protected function isLimited(): bool { 308 | return $this->limit > 0; 309 | } 310 | 311 | protected function follow(): bool { 312 | if (array_key_exists('f', $this->config['options']) || 313 | array_key_exists('follow', $this->config['options'])) { 314 | return true; 315 | } 316 | 317 | return false; 318 | } 319 | 320 | /** 321 | * @return self Returns itself 322 | * 323 | * @throws Exception on error 324 | */ 325 | protected function fetchObjectIDsByTitles(): self { 326 | if ($this->hasTitleFilter()) { 327 | $objectIDs = $this->useIdoitAPI()->fetchObjectIDsByTitles($this->filterByTitles); 328 | 329 | $this->filterByIDs = array_merge($this->filterByIDs, $objectIDs); 330 | } 331 | return $this; 332 | } 333 | 334 | /** 335 | * @return self Returns itself 336 | * 337 | * @throws Exception on error 338 | */ 339 | protected function readLogs(): self { 340 | $this->logs = []; 341 | 342 | if (!$this->hasAnyFilter()) { 343 | $this->logs = $this->useIdoitAPIFactory()->getCMDBLogbook()->read(null, $this->hardLimit); 344 | } else { 345 | $since = null; 346 | 347 | if ($this->hasDateTimeFilter()) { 348 | $since = $this->filterByDateTime; 349 | } 350 | 351 | if ($this->hasIDFilter()) { 352 | foreach ($this->filterByIDs as $objectID) { 353 | $this->logs = array_merge( 354 | $this->logs, 355 | $this->useIdoitAPIFactory()->getCMDBLogbook()->readByObject( 356 | $objectID, 357 | $since, 358 | $this->hardLimit 359 | ) 360 | ); 361 | } 362 | 363 | usort( 364 | $this->logs, 365 | [self::class, 'sortLogsByDate'] 366 | ); 367 | } else { 368 | $this->logs = $this->useIdoitAPIFactory()->getCMDBLogbook()->read( 369 | $since, 370 | $this->hardLimit 371 | ); 372 | } 373 | } 374 | 375 | if ($this->isLimited()) { 376 | $this->logs = array_slice( 377 | $this->logs, 378 | -$this->limit 379 | ); 380 | } 381 | 382 | return $this; 383 | } 384 | 385 | /** 386 | * @return self Returns itself 387 | * 388 | * @throws Exception on error 389 | */ 390 | protected function printLogs(): self { 391 | switch (count($this->logs)) { 392 | case 0: 393 | $this->log->printAsMessage()->debug( 394 | 'No log events' 395 | ); 396 | break; 397 | case 1: 398 | $this->log->printAsMessage()->debug( 399 | '1 log event:' 400 | ); 401 | break; 402 | default: 403 | $this->log->printAsMessage()->debug( 404 | '%s log events:', 405 | count($this->logs) 406 | ); 407 | break; 408 | } 409 | 410 | foreach ($this->logs as $log) { 411 | $this->printLog($log); 412 | } 413 | 414 | return $this; 415 | } 416 | 417 | /** 418 | * Sort logs by date 419 | * 420 | * @param array $logA 421 | * @param array $logB 422 | * 423 | * @return int 424 | */ 425 | public static function sortLogsByDate(array $logA, $logB): int { 426 | if ($logA['date'] === $logB['date']) { 427 | return 0; 428 | } 429 | 430 | $logATimeStamp = strtotime($logA['date']); 431 | $logBTimeStamp = strtotime($logB['date']); 432 | 433 | return $logATimeStamp < $logBTimeStamp ? -1 : 1; 434 | } 435 | 436 | protected function printLog(array $log) { 437 | $id = $log['logbook_id']; 438 | $timestamp = $log['date']; 439 | $username = $log['username']; 440 | $objectTitle = $log['object_title']; 441 | $objectID = $log['object_id']; 442 | 443 | if (array_key_exists($log['event'], $this->events)) { 444 | $event = $this->events[$log['event']]; 445 | } else { 446 | $event = $log['event']; 447 | } 448 | 449 | $this->log 450 | ->printAsOutput() 451 | ->info( 452 | <<< EOF 453 | #$id $username @ $timestamp 454 | $objectTitle [$objectID] 455 | $event 456 | 457 | EOF 458 | ); 459 | } 460 | 461 | /** 462 | * @throws Exception on error 463 | */ 464 | protected function keepFollowing() { 465 | $this->filterByDateTime = date('c'); 466 | 467 | if (count($this->logs) > 0) { 468 | $lastLog = end($this->logs); 469 | $timestamp = strtotime($lastLog['date']) + 1; 470 | $this->filterByDateTime = date('c', $timestamp); 471 | } 472 | 473 | while (true) { 474 | $this 475 | ->readLogs() 476 | ->printLogs(); 477 | 478 | if (count($this->logs) > 0) { 479 | $lastLog = end($this->logs); 480 | $timestamp = strtotime($lastLog['date']) + 1; 481 | $this->filterByDateTime = date('c', $timestamp); 482 | } 483 | 484 | sleep(2); 485 | } 486 | } 487 | 488 | /** 489 | * Print usage of command 490 | * 491 | * @return self Returns itself 492 | */ 493 | public function printUsage(): self { 494 | $this->log->info( 495 | <<< EOF 496 | %3\$s 497 | 498 | USAGE 499 | \$ %1\$s %2\$s [OPTIONS] 500 | 501 | COMMAND OPTIONS 502 | -f, --follow Start a continuous stream of logs 503 | --id=ID Filter logs by numeric object identifier 504 | Repeat to filter by more than one identifiers 505 | -n LIMIT, Limit to last number of logs 506 | --number=LIMIT Note: There is a hard limit set to a maximum amount of 507 | %4\$s entries to prevent server-side errors 508 | --since=TIME Filter logs since a specific date/time 509 | May be everything that can be interpreted as a date/time 510 | --title=TITLE Filter logs by object title 511 | Repeat to filter by more than one titles 512 | 513 | Any combination of options is allowed. 514 | 515 | COMMON OPTIONS 516 | -c FILE, Include settings stored in a JSON-formatted 517 | --config=FILE configuration file FILE; repeat option for more 518 | than one FILE 519 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 520 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 521 | repeat option for more than one KEY 522 | 523 | --no-colors Do not print colored messages 524 | -q, --quiet Do not output messages, only errors 525 | -v, --verbose Be more verbose 526 | 527 | -h, --help Print this help or information about a 528 | specific command 529 | --version Print version information 530 | 531 | -y, --yes No user interaction required; answer questions 532 | automatically with default values 533 | 534 | EXAMPLES 535 | # Read all logs at once: 536 | \$ %1\$s %2\$s 537 | 538 | # Read logs about 2 objects by their identifiers: 539 | \$ %1\$s %2\$s --id 23 --id 42 540 | 541 | # Read logs about an objects by its title: 542 | \$ %1\$s %2\$s --title "host001.example.com" 543 | 544 | # Follow: 545 | \$ %1\$s %2\$s -f 546 | 547 | # Print only logs since today: 548 | \$ %1\$s %2\$s --since today 549 | 550 | # Or since a specific date: 551 | \$ %1\$s %2\$s --since 2018-01-01 552 | EOF 553 | , 554 | $this->config['composer']['extra']['name'], 555 | $this->getName(), 556 | $this->getDescription(), 557 | $this->hardLimit 558 | ); 559 | 560 | return $this; 561 | } 562 | 563 | } 564 | -------------------------------------------------------------------------------- /src/Command/NextIP.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | 32 | /** 33 | * Command "nextip" 34 | */ 35 | class NextIP extends Command { 36 | 37 | protected $freeIPAddresses = []; 38 | 39 | /** 40 | * Execute command 41 | * 42 | * @return self Returns itself 43 | * 44 | * @throws Exception on error 45 | */ 46 | public function execute(): self { 47 | $this->log 48 | ->printAsMessage() 49 | ->info($this->getDescription()) 50 | ->printEmptyLine(); 51 | 52 | switch (count($this->config['arguments'])) { 53 | case 0: 54 | if ($this->useUserInteraction()->isInteractive() === false) { 55 | throw new BadMethodCallException( 56 | 'No object, no IP' 57 | ); 58 | } 59 | 60 | $object = $this->askForObject(); 61 | $objectID = (int) $object['id']; 62 | break; 63 | case 1: 64 | $object = $this->useIdoitAPI()->identifyObject( 65 | $this->config['arguments'][0] 66 | ); 67 | 68 | $objectID = (int) $object['id']; 69 | break; 70 | default: 71 | throw new BadMethodCallException( 72 | 'Too many arguments; please provide only one object title or numeric identifier' 73 | ); 74 | } 75 | 76 | $next = $this->useIdoitAPIFactory()->getSubnet()->load($objectID)->next(); 77 | 78 | $this->log 79 | ->printAsOutput() 80 | ->info($next); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Print usage of command 87 | * 88 | * @return self Returns itself 89 | */ 90 | public function printUsage(): self { 91 | $this->log->info( 92 | <<< EOF 93 | %3\$s 94 | 95 | USAGE 96 | \$ %1\$s %2\$s [OPTIONS] [SUBNET] 97 | 98 | ARGUMENTS 99 | SUBNET Object title or numeric identifier 100 | Must be a "layer-3-net" object 101 | Only works for IPv4 subnets 102 | 103 | COMMON OPTIONS 104 | -c FILE, Include settings stored in a JSON-formatted 105 | --config=FILE configuration file FILE; repeat option for more 106 | than one FILE 107 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 108 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 109 | repeat option for more than one KEY 110 | 111 | --no-colors Do not print colored messages 112 | -q, --quiet Do not output messages, only errors 113 | -v, --verbose Be more verbose 114 | 115 | -h, --help Print this help or information about a 116 | specific command 117 | --version Print version information 118 | 119 | -y, --yes No user interaction required; answer questions 120 | automatically with default values 121 | 122 | EXAMPLES 123 | # Print next IPv4 for subnet by its title: 124 | \$ %1\$s %2\$s "Global v4" 125 | \$ %1\$s %2\$s Global\\ v4 126 | 127 | # …or by its numeric identifier: 128 | \$ %1\$s %2\$s 20 129 | 130 | # If argument SUBNET is omitted you'll be asked for it: 131 | \$ %1\$s %2\$s 132 | Object? Global v4 133 | EOF 134 | , 135 | $this->config['composer']['extra']['name'], 136 | $this->getName(), 137 | $this->getDescription() 138 | ); 139 | 140 | return $this; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/Command/Search.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | 32 | /** 33 | * Command "search" 34 | */ 35 | class Search extends Command { 36 | 37 | /** 38 | * Execute command 39 | * 40 | * @return self Returns itself 41 | * 42 | * @throws Exception on error 43 | */ 44 | public function execute(): self { 45 | $this->log 46 | ->printAsMessage() 47 | ->info($this->getDescription()) 48 | ->printEmptyLine(); 49 | 50 | switch (count($this->config['arguments'])) { 51 | case 0: 52 | if ($this->useUserInteraction()->isInteractive() === false) { 53 | throw new BadMethodCallException( 54 | 'No query, no search' 55 | ); 56 | } 57 | 58 | $query = $this->useUserInteraction()->askQuestion('Query?'); 59 | break; 60 | case 1: 61 | $query = $this->config['arguments'][0]; 62 | break; 63 | default: 64 | throw new BadMethodCallException( 65 | 'Too many arguments; please provide only one query' 66 | ); 67 | } 68 | 69 | if ($query === '') { 70 | throw new BadMethodCallException( 71 | 'Query is required.' 72 | ); 73 | } 74 | 75 | $results = $this->useIdoitAPIFactory()->getCMDB()->search($query); 76 | 77 | switch (count($results)) { 78 | case 0: 79 | $this->log 80 | ->printAsMessage() 81 | ->info('Nothing found'); 82 | break; 83 | case 1: 84 | $this->log 85 | ->printAsMessage() 86 | ->info('Found 1 result'); 87 | break; 88 | default: 89 | $this->log 90 | ->printAsMessage() 91 | ->info('Found %s results', count($results)); 92 | break; 93 | } 94 | 95 | $baseLink = strstr($this->config['api']['url'], '/src/jsonrpc.php', true); 96 | 97 | foreach ($results as $result) { 98 | $this->log 99 | ->printAsOutput() 100 | ->printEmptyLine() 101 | ->info('%s', trim($result['value'])) 102 | ->info('Source: %s [%s]', $result['key'], $result['type']) 103 | ->info('Link: %s', $baseLink . $result['link']); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Print usage of command 111 | * 112 | * @return self Returns itself 113 | */ 114 | public function printUsage(): self { 115 | $this->log->info( 116 | <<< EOF 117 | %3\$s 118 | 119 | USAGE 120 | \$ %1\$s %2\$s [OPTIONS] QUERY 121 | 122 | ARGUMENTS 123 | QUERY What are you looking for? 124 | 125 | COMMON OPTIONS 126 | -c FILE, Include settings stored in a JSON-formatted 127 | --config=FILE configuration file FILE; repeat option for more 128 | than one FILE 129 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 130 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 131 | repeat option for more than one KEY 132 | 133 | --no-colors Do not print colored messages 134 | -q, --quiet Do not output messages, only errors 135 | -v, --verbose Be more verbose 136 | 137 | -h, --help Print this help or information about a 138 | specific command 139 | --version Print version information 140 | 141 | -y, --yes No user interaction required; answer questions 142 | automatically with default values 143 | 144 | EXAMPLES 145 | \$ %1\$s %2\$s mylittleserver 146 | \$ %1\$s %2\$s "My little server" 147 | \$ %1\$s %2\$s My\\ little\\ server 148 | \$ %1\$s %2\$s 10.42.23.1 149 | EOF 150 | , 151 | $this->config['composer']['extra']['name'], 152 | $this->getName(), 153 | $this->getDescription() 154 | ); 155 | 156 | return $this; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/Command/Show.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | 32 | /** 33 | * Command "show" 34 | */ 35 | class Show extends Command { 36 | 37 | /** 38 | * Execute command 39 | * 40 | * @return self Returns itself 41 | * 42 | * @throws Exception on error 43 | */ 44 | public function execute(): self { 45 | $this->log 46 | ->printAsMessage() 47 | ->info($this->getDescription()) 48 | ->printEmptyLine(); 49 | 50 | $object = $this->loadObject($this->config['arguments']); 51 | 52 | $this->printOutput($object); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Load object based on CLI argument 59 | * 60 | * @param array $arguments CLI arguments 61 | * 62 | * @return array Everything about the object 63 | * 64 | * @throws Exception on error 65 | */ 66 | protected function loadObject(array $arguments): array { 67 | switch (count($arguments)) { 68 | case 0: 69 | if ($this->useUserInteraction()->isInteractive() === false) { 70 | throw new BadMethodCallException( 71 | 'No object, no output' 72 | ); 73 | } 74 | 75 | $object = $this->askForObject(); 76 | 77 | $this->log->debug('Loading…'); 78 | 79 | return $this 80 | ->useIdoitAPIFactory() 81 | ->getCMDBObject() 82 | ->load((int) $object['id']); 83 | case 1: 84 | if (is_numeric($arguments[0])) { 85 | $objectID = (int) $arguments[0]; 86 | 87 | if ($objectID <= 0) { 88 | throw new Exception( 89 | 'Invalid object. Please specify an numeric identifier' 90 | ); 91 | } 92 | 93 | $this->log->debug('Loading…'); 94 | 95 | return $this 96 | ->useIdoitAPIFactory() 97 | ->getCMDBObject() 98 | ->load($objectID); 99 | } else { 100 | $objects = $this 101 | ->useIdoitAPIFactory() 102 | ->getCMDBObjects() 103 | ->read(['title' => $arguments[0]]); 104 | 105 | switch (count($objects)) { 106 | case 0: 107 | if ($this->useUserInteraction()->isInteractive() === false) { 108 | throw new BadMethodCallException( 109 | 'Object not found' 110 | ); 111 | } 112 | 113 | $this->log->warning('Object not found'); 114 | $object = $this->askForObject(); 115 | 116 | $this->log->debug('Loading…'); 117 | 118 | return $this 119 | ->useIdoitAPIFactory() 120 | ->getCMDBObject() 121 | ->load((int) $object['id']); 122 | case 1: 123 | $objectID = (int) $objects[0]['id']; 124 | 125 | $this->log->debug('Loading…'); 126 | 127 | return $this 128 | ->useIdoitAPIFactory() 129 | ->getCMDBObject() 130 | ->load($objectID); 131 | default: 132 | $this->log 133 | ->debug('Found %s objects', count($objects)) 134 | ->printEmptyLine(); 135 | 136 | foreach ($objects as $object) { 137 | $this->log->info( 138 | '%s: %s', 139 | $object['id'], 140 | $object['title'] 141 | ); 142 | } 143 | 144 | $this->log->printEmptyLine(); 145 | 146 | if ($this->useUserInteraction()->isInteractive() === false) { 147 | throw new BadMethodCallException( 148 | 'Object selection is ambiguous. Select one by title or numeric identifier' 149 | ); 150 | } 151 | 152 | while (true) { 153 | $answer = $this->useUserInteraction()->askQuestion( 154 | 'Select object by title or numeric identifier:' 155 | ); 156 | 157 | foreach ($objects as $object) { 158 | if (is_numeric($answer) && 159 | (int) $answer >= 0 && 160 | (int) $answer === (int) $object['id']) { 161 | $this->log->debug('Loading…'); 162 | 163 | return $this 164 | ->useIdoitAPIFactory() 165 | ->getCMDBObject() 166 | ->load((int) $answer); 167 | } elseif ($answer === $object['title']) { 168 | $this->log->debug('Loading…'); 169 | 170 | return $this 171 | ->useIdoitAPIFactory() 172 | ->getCMDBObject() 173 | ->load((int) $object['id']); 174 | } 175 | } 176 | 177 | $this->log->warning('Please try again.'); 178 | } 179 | break; 180 | } 181 | } 182 | break; 183 | default: 184 | throw new BadMethodCallException( 185 | 'Too many arguments; please provide only one object title or numeric identifier' 186 | ); 187 | } 188 | 189 | return []; 190 | } 191 | 192 | /** 193 | * Print output 194 | * 195 | * @param array $object Everything about the object 196 | * 197 | * @throws Exception on error 198 | */ 199 | protected function printOutput(array $object) { 200 | $this->log->printAsOutput() 201 | ->info('Title: %s', $object['title']) 202 | ->info('ID: %s', $object['id']) 203 | ->info('Type: %s', $object['type_title']) 204 | ->info('SYS-ID: %s', $object['sysid']) 205 | ->info('CMDB status: %s', $object['cmdb_status_title']) 206 | ->info('Created: %s', $object['created']) 207 | ->info('Updated: %s', $object['updated']); 208 | 209 | $categoryTypes = ['catg', 'cats', 'custom']; 210 | 211 | $blacklistedCategories = array_merge( 212 | $this 213 | ->useIdoitAPIFactory() 214 | ->getCMDBCategoryInfo() 215 | ->getVirtualCategoryConstants(), 216 | [ 217 | 'C__CATG__OVERVIEW', 218 | 'C__CATG__RELATION', 219 | 'C__CATG__LOGBOOK' 220 | ] 221 | ); 222 | 223 | foreach ($categoryTypes as $categoryType) { 224 | if (!array_key_exists($categoryType, $object)) { 225 | continue; 226 | } 227 | 228 | foreach ($object[$categoryType] as $category) { 229 | if (in_array($category['const'], $blacklistedCategories)) { 230 | continue; 231 | } 232 | 233 | $this->log 234 | ->printAsMessage() 235 | ->printEmptyLine() 236 | ->info( 237 | '%s [%s]', 238 | $category['title'], 239 | $category['const'] 240 | ); 241 | 242 | switch (count($category['entries'])) { 243 | case 0: 244 | $this->log->printAsMessage()->info( 245 | 'No entries found' 246 | ); 247 | continue 2; 248 | case 1: 249 | $this->log->printAsMessage()->info( 250 | '1 entry found:' 251 | ); 252 | break; 253 | default: 254 | $this->log->printAsMessage()->info( 255 | '%s entries found:', 256 | count($category['entries']) 257 | ); 258 | break; 259 | } 260 | 261 | try { 262 | $categoryInfo = $this->useCache()->getCategoryInfo($category['const']); 263 | } catch (Exception $e) { 264 | $this->log->printAsMessage()->notice($e->getMessage()); 265 | continue; 266 | } 267 | 268 | foreach ($category['entries'] as $entry) { 269 | $this->log->printAsMessage()->printEmptyLine(); 270 | 271 | foreach ($entry as $attribute => $value) { 272 | if ($attribute === 'objID') { 273 | continue; 274 | } 275 | 276 | if ($attribute === 'id') { 277 | $this->log 278 | ->printAsOutput() 279 | ->debug('#%s', $value); 280 | continue; 281 | } 282 | 283 | $this->handleAttribute() 284 | ->load($categoryInfo['properties'][$attribute]); 285 | 286 | if ($this->handleAttribute()->ignore()) { 287 | continue; 288 | } 289 | 290 | $value = $this->handleAttribute() 291 | ->encode($value); 292 | 293 | if ($value === '') { 294 | $value = '-'; 295 | } 296 | 297 | $this->log->printAsOutput()->info( 298 | '%s: %s', 299 | $categoryInfo['properties'][$attribute]['title'], 300 | $value 301 | ); 302 | } 303 | } 304 | } 305 | } 306 | } 307 | 308 | /** 309 | * Print usage of command 310 | * 311 | * @return self Returns itself 312 | */ 313 | public function printUsage(): self { 314 | $this->log->info( 315 | <<< EOF 316 | %3\$s 317 | 318 | USAGE 319 | \$ %1\$s %2\$s [OPTIONS] [OBJECT] 320 | 321 | ARGUMENTS 322 | OBJECT Object title or numeric identifier 323 | 324 | COMMON OPTIONS 325 | -c FILE, Include settings stored in a JSON-formatted 326 | --config=FILE configuration file FILE; repeat option for more 327 | than one FILE 328 | -s KEY=VALUE, Add runtime setting KEY with its VALUE; separate 329 | --setting=KEY=VALUE nested keys with ".", for example "key1.key2=123"; 330 | repeat option for more than one KEY 331 | 332 | --no-colors Do not print colored messages 333 | -q, --quiet Do not output messages, only errors 334 | -v, --verbose Be more verbose 335 | 336 | -h, --help Print this help or information about a 337 | specific command 338 | --version Print version information 339 | 340 | -y, --yes No user interaction required; answer questions 341 | automatically with default values 342 | 343 | EXAMPLES 344 | # %3\$s by its title: 345 | \$ %1\$s %2\$s mylittleserver 346 | 347 | # …or by its numeric identifier: 348 | \$ %1\$s %2\$s 42 349 | 350 | Handle spaces in title with care: 351 | \$ %1\$s %2\$s "My little server" 352 | \$ %1\$s %2\$s My\\ little\\ server 353 | 354 | # If argument OBJECT is omitted you'll be asked for it: 355 | \$ %1\$s %2\$s 356 | Object? host01.example.com 357 | EOF 358 | , 359 | $this->config['composer']['extra']['name'], 360 | $this->getName(), 361 | $this->getDescription() 362 | ); 363 | 364 | return $this; 365 | } 366 | 367 | } 368 | -------------------------------------------------------------------------------- /src/Command/Status.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | 31 | /** 32 | * Command "status" 33 | */ 34 | class Status extends Command { 35 | 36 | protected $idoitInfo = []; 37 | protected $apiInfo = []; 38 | 39 | /** 40 | * Execute command 41 | * 42 | * @return self Returns itself 43 | * 44 | * @throws Exception on error 45 | */ 46 | public function execute(): self { 47 | $this->log 48 | ->printAsMessage() 49 | ->info($this->getDescription()) 50 | ->printEmptyLine(); 51 | 52 | return $this 53 | ->getIdoitInfo() 54 | ->getAPIVersion() 55 | ->printStatus(); 56 | } 57 | 58 | /** 59 | * @return self Returns itself 60 | * 61 | * @throws Exception on error 62 | */ 63 | protected function getIdoitInfo(): self { 64 | $this->idoitInfo = $this->useIdoitAPIFactory()->getCMDB()->readVersion(); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @return self Returns itself 71 | * 72 | * @throws Exception on error 73 | */ 74 | protected function getAPIVersion(): self { 75 | try { 76 | $addOns = $this->useIdoitAPIFactory()->getCMDB()->getAddOns(); 77 | 78 | foreach ($addOns as $addOn) { 79 | if ($addOn['key'] === 'api') { 80 | $this->apiInfo = $addOn; 81 | break; 82 | } 83 | } 84 | } catch (Exception $e) { 85 | // Suppress any exception… 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | protected function printStatus(): self { 92 | $this->log 93 | ->printAsOutput() 94 | ->info('About i-doit:') 95 | ->info( 96 | 'Version: i-doit %s %s', 97 | strtolower($this->idoitInfo['type']), 98 | $this->idoitInfo['version'] 99 | ) 100 | ->info('Tenant: %s', $this->idoitInfo['login']['mandator']) 101 | ->info( 102 | 'Link: %s', 103 | str_replace('src/jsonrpc.php', '', $this->config['api']['url']) 104 | ) 105 | ->info('API version: %s', $this->apiInfo['version']) 106 | ->info('API entry point: %s', $this->config['api']['url']) 107 | ->printEmptyLine() 108 | ->info('About this script:') 109 | ->info('Name: %s', $this->config['composer']['extra']['name']) 110 | ->info('Description: %s', $this->config['composer']['description']) 111 | ->info('Version: %s', $this->config['composer']['extra']['version']) 112 | ->info('Website: %s', $this->config['composer']['homepage']) 113 | ->info('License: %s', $this->config['composer']['license']) 114 | ->printEmptyLine() 115 | ->info('About you:') 116 | ->info('Name: %s', $this->idoitInfo['login']['name']) 117 | ->info('User name: %s', $this->idoitInfo['login']['username']) 118 | ->info('E-mail: %s', $this->idoitInfo['login']['mail']) 119 | ->info('Language: %s', $this->idoitInfo['login']['language']); 120 | 121 | return $this; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/Command/Types.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Command; 28 | 29 | use \Exception; 30 | 31 | /** 32 | * Command "types" 33 | */ 34 | class Types extends Command { 35 | 36 | /** 37 | * Execute command 38 | * 39 | * @return self Returns itself 40 | * 41 | * @throws Exception on error 42 | */ 43 | public function execute(): self { 44 | $this->log 45 | ->printAsMessage() 46 | ->info($this->getDescription()) 47 | ->printEmptyLine(); 48 | 49 | $types = $this->useCache()->getObjectTypes(); 50 | 51 | $types = array_filter($types, [$this, 'filterObjectTypes']); 52 | 53 | usort($types, [$this, 'sort']); 54 | 55 | $groups = $this->group($types); 56 | 57 | $this->formatGroups($groups); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Accept only object types with status "normal" [2] 64 | * 65 | * @param array $type Object type information 66 | * 67 | * @return bool 68 | */ 69 | protected function filterObjectTypes(array $type): bool { 70 | return $type['status'] === '2'; 71 | } 72 | 73 | /** 74 | * Group object types by localized group names 75 | * 76 | * @param array $types Object type information 77 | * 78 | * @return array 79 | */ 80 | protected function group(array $types): array { 81 | $groups = []; 82 | 83 | foreach ($types as $type) { 84 | if (!array_key_exists($type['type_group_title'], $groups)) { 85 | $groups[$type['type_group_title']] = []; 86 | } 87 | 88 | $groups[$type['type_group_title']][] = $type; 89 | } 90 | 91 | return $groups; 92 | } 93 | 94 | /** 95 | * Print object type groups 96 | * 97 | * @param array $groups Object type groups 98 | */ 99 | protected function formatGroups(array $groups) { 100 | switch (count($groups)) { 101 | case 0: 102 | $this->log 103 | ->printAsMessage() 104 | ->debug('No groups found'); 105 | break; 106 | case 1: 107 | $this->log 108 | ->printAsMessage() 109 | ->debug('1 group type found'); 110 | break; 111 | default: 112 | $this->log 113 | ->printAsMessage() 114 | ->debug('%s groups found', count($groups)); 115 | break; 116 | } 117 | 118 | foreach ($groups as $group => $types) { 119 | $this->log 120 | ->info('%s:', $group); 121 | 122 | $this->formatList($types); 123 | 124 | $this->log 125 | ->printAsMessage() 126 | ->printEmptyLine(); 127 | } 128 | } 129 | 130 | /** 131 | * Print object types 132 | * 133 | * @param array $types List of object types 134 | */ 135 | protected function formatList(array $types) { 136 | switch (count($types)) { 137 | case 0: 138 | $this->log 139 | ->printAsMessage() 140 | ->debug('No object types found'); 141 | break; 142 | case 1: 143 | $this->log 144 | ->printAsMessage() 145 | ->debug('1 object type found'); 146 | break; 147 | default: 148 | $this->log 149 | ->printAsMessage() 150 | ->debug('%s object types found', count($types)); 151 | break; 152 | } 153 | 154 | $this->log->printEmptyLine(); 155 | 156 | foreach ($types as $type) { 157 | $this->log 158 | ->printAsOutput() 159 | ->info($this->format($type)); 160 | } 161 | } 162 | 163 | /** 164 | * Format object type 165 | * 166 | * @param array $type Object type information 167 | * 168 | * @return string 169 | */ 170 | protected function format(array $type): string { 171 | return sprintf( 172 | '%s [%s]', 173 | $type['title'], 174 | $type['const'] 175 | ); 176 | } 177 | 178 | /** 179 | * Sort object types in alphabetical order by their localized titles 180 | * 181 | * @param array $a Object type information 182 | * @param array $b Object type information 183 | * 184 | * @return int 185 | */ 186 | protected function sort(array $a, array $b): int { 187 | return strcmp($a['title'], $b['title']); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/Service/Cache.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | use \DirectoryIterator; 30 | use \Exception; 31 | use \RuntimeException; 32 | 33 | /** 34 | * Cache files 35 | */ 36 | class Cache extends Service { 37 | 38 | /** 39 | * Cached object types 40 | * 41 | * @var array Indexed array of associative arrays 42 | */ 43 | protected $cacheObjectTypes = []; 44 | 45 | /** 46 | * Cached categories 47 | * 48 | * @var array Indexed array of associative arrays 49 | */ 50 | protected $cacheCategories = []; 51 | 52 | /** 53 | * Cached assignments between categories and object types 54 | * 55 | * @var array Indexed array of associative arrays 56 | */ 57 | protected $cacheAssignedCategories = []; 58 | 59 | /** 60 | * Cache directory for current i-doit host 61 | * 62 | * @var string 63 | */ 64 | protected $hostDir; 65 | 66 | /** 67 | * Is cache for current host available? 68 | * 69 | * @return bool 70 | * 71 | * @throws Exception on error 72 | */ 73 | public function isCached(): bool { 74 | $hostDir = $this->getHostDir(); 75 | 76 | if (!is_dir($hostDir)) { 77 | return false; 78 | } 79 | 80 | $dir = new DirectoryIterator($hostDir); 81 | 82 | foreach ($dir as $file) { 83 | if ($file->isDot() || $file->isDir()) { 84 | continue; 85 | } 86 | 87 | if ($file->isFile() === false) { 88 | continue; 89 | } 90 | 91 | if ($this->config['cacheLifetime'] > 0 && 92 | (time() - $this->config['cacheLifetime'] > $file->getCTime())) { 93 | return false; 94 | } 95 | 96 | // First file is valid – this is all we need to know: 97 | return true; 98 | } 99 | 100 | return false; 101 | } 102 | 103 | /** 104 | * Reads list of object types from cache 105 | * 106 | * @return array 107 | * 108 | * @throws Exception on error 109 | */ 110 | public function getObjectTypes(): array { 111 | $hostDir = $this->getHostDir(); 112 | $filePath = $hostDir . '/object_types'; 113 | return $this->unserializeFromFile($filePath); 114 | } 115 | 116 | /** 117 | * Converts an object type title into a object type constant 118 | * 119 | * @param string $title Object type title 120 | * 121 | * @return string Object type constant, otherwise \Exception is thrown 122 | * 123 | * @throws Exception on error 124 | */ 125 | public function getObjectTypeConstantByTitle(string $title) { 126 | $objectTypes = $this->getObjectTypes(); 127 | 128 | foreach ($objectTypes as $objectType) { 129 | if (strtolower($objectType['title']) === strtolower($title)) { 130 | return $objectType['const']; 131 | } 132 | } 133 | 134 | throw new RuntimeException(sprintf( 135 | 'Unable to find constant for object type "%s"', 136 | $title 137 | )); 138 | } 139 | 140 | /** 141 | * Reads information about a category from cache 142 | * 143 | * @param string $categoryConst Category constant 144 | * 145 | * @return array 146 | * 147 | * @throws Exception on error 148 | */ 149 | public function getCategoryInfo(string $categoryConst): array { 150 | $hostDir = $this->getHostDir(); 151 | $filePath = $hostDir . '/category__' . $categoryConst; 152 | return $this->unserializeFromFile($filePath); 153 | } 154 | 155 | /** 156 | * Reads list of categories from cache which are assigned to an object type 157 | * 158 | * @param string $type Object type constant 159 | * 160 | * @return array ['catg' => [['id' => 1, …], ['id' => 1, …]], 'cats' => …] 161 | * 162 | * @throws Exception on error 163 | */ 164 | public function getAssignedCategories(string $type): array { 165 | $hostDir = $this->getHostDir(); 166 | $filePath = $hostDir . '/object_type__' . $type; 167 | return $this->unserializeFromFile($filePath); 168 | } 169 | 170 | /** 171 | * Reads a list of categories from cache 172 | * 173 | * @return array 174 | * 175 | * @throws Exception on error 176 | */ 177 | public function getCategories(): array { 178 | $categories = []; 179 | $hostDir = $this->getHostDir(); 180 | 181 | $dir = new DirectoryIterator($hostDir); 182 | 183 | foreach ($dir as $file) { 184 | if ($file->isFile() === false) { 185 | continue; 186 | } 187 | 188 | if (strpos($file->getFilename(), 'category__') !== 0) { 189 | continue; 190 | } 191 | 192 | $filePath = $hostDir . '/' . $file->getFilename(); 193 | 194 | $categories[] = $this->unserializeFromFile($filePath); 195 | } 196 | 197 | return $categories; 198 | } 199 | 200 | /** 201 | * Gets cache directory for current i-doit host 202 | * 203 | * @return string 204 | * 205 | * @throws Exception when configuration settings are missing 206 | */ 207 | public function getHostDir(): string { 208 | if (!array_key_exists('api', $this->config) || 209 | !array_key_exists('url', $this->config['api'])) { 210 | throw new Exception(sprintf( 211 | 'No proper configuration found' . PHP_EOL . 212 | 'Run "%s init" to create configuration settings', 213 | $this->config['composer']['extra']['name'] 214 | )); 215 | } 216 | 217 | if (!isset($this->hostDir)) { 218 | $this->hostDir = $this->config['dataDir'] . '/' . 219 | sha1($this->config['api']['url']); 220 | } 221 | 222 | return $this->hostDir; 223 | } 224 | 225 | /** 226 | * Read file and create PHP code out of it 227 | * 228 | * @param string $filePath Path to file 229 | * 230 | * @return mixed 231 | * 232 | * @throws RuntimeException on error 233 | */ 234 | protected function unserializeFromFile(string $filePath) { 235 | if (!is_readable($filePath)) { 236 | throw new RuntimeException(sprintf( 237 | 'File "%s" not found or not accessible', 238 | $filePath 239 | )); 240 | } 241 | 242 | $fileContent = file_get_contents($filePath); 243 | 244 | if (!is_string($fileContent)) { 245 | throw new RuntimeException(sprintf( 246 | 'Unable to read file "%s"', 247 | $filePath 248 | )); 249 | } 250 | 251 | return unserialize($fileContent); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /src/Service/IdoitAPI.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | use \RuntimeException; 32 | 33 | /** 34 | * i-doit API 35 | */ 36 | class IdoitAPI extends Service { 37 | 38 | /** 39 | * @var IdoitAPIFactory 40 | */ 41 | protected $idoitAPIFactory; 42 | 43 | /** 44 | * Setup service 45 | * 46 | * @param IdoitAPIFactory $idoitAPIFactory 47 | * 48 | * @return self Returns itself 49 | */ 50 | public function setUp(IdoitAPIFactory $idoitAPIFactory): self{ 51 | $this->idoitAPIFactory = $idoitAPIFactory; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Fetches objects from i-doit 58 | * 59 | * @param array $filter Associative array, see CMDBObjects::read() 60 | * @param array $categories List of category constants for more information 61 | * @param int $objectCount How many objects will be fetched? Defaults to 0 (ignore it) 62 | * 63 | * @return array Keys: object identifiers; values: object attributes 64 | * 65 | * @throws Exception on error 66 | */ 67 | public function fetchObjects(array $filter, array $categories = [], int $objectCount = 0): array { 68 | $limit = $this->config['limitBatchRequests']; 69 | 70 | $objects = []; 71 | $offset = 0; 72 | 73 | if (array_key_exists('title', $filter) && strpos($filter['title'], '*') !== false) { 74 | $readFilter = array_filter($filter, function ($key) { 75 | return $key !== 'title'; 76 | }, ARRAY_FILTER_USE_KEY); 77 | } else { 78 | $readFilter = $filter; 79 | } 80 | 81 | while (true) { 82 | if ($limit > 0) { 83 | $result = $this->idoitAPIFactory->getCMDBObjects()->read($readFilter, $limit, (int) $offset); 84 | 85 | if ($objectCount > 0 && $objectCount >= $limit) { 86 | if ($offset === 0) { 87 | $this->log->debug( 88 | 'Fetch first %s objects from %s to %s', 89 | $limit, 90 | $offset + 1, 91 | $offset + $limit 92 | ); 93 | } elseif (($objectCount - $offset) === 1) { 94 | $this->log->debug( 95 | 'Fetch last object' 96 | ); 97 | } elseif (($objectCount - $offset) > 1 && ($offset + $limit) > $objectCount) { 98 | $this->log->debug( 99 | 'Fetch last %s objects', 100 | $objectCount - $offset 101 | ); 102 | } elseif (($objectCount - $offset) > 1) { 103 | $this->log->debug( 104 | 'Fetch next %s objects from %s to %s', 105 | $limit, 106 | $offset + 1, 107 | $offset + $limit 108 | ); 109 | } 110 | } 111 | 112 | $offset += $limit; 113 | } else { 114 | $result = $this->idoitAPIFactory->getCMDBObjects()->read($readFilter); 115 | } 116 | 117 | if (count($result) === 0) { 118 | break; 119 | } 120 | 121 | if (array_key_exists('title', $filter) && strpos($filter['title'], '*') !== false) { 122 | $result = array_filter($result, function ($object) use ($filter) { 123 | return fnmatch($filter['title'], $object['title']); 124 | }); 125 | } 126 | 127 | $objectIDs = []; 128 | 129 | foreach ($result as $object) { 130 | $objectID = (int) $object['id']; 131 | 132 | $objectIDs[] = $objectID; 133 | 134 | $objects[$objectID] = [ 135 | 'id' => $objectID, 136 | 'title' => $object['title'], 137 | 'type' => (int) $object['type'], 138 | 'type_title' => $object['type_title'], 139 | ]; 140 | } 141 | 142 | if (count($categories) > 0) { 143 | foreach ($categories as $categoryConstant) { 144 | $categoryResults = $this 145 | ->idoitAPIFactory 146 | ->getCMDBCategory() 147 | ->batchRead($objectIDs, [$categoryConstant]); 148 | 149 | $count = 0; 150 | foreach ($categoryResults as $categoryResult) { 151 | $objects[$objectIDs[$count]][$categoryConstant] = $categoryResult; 152 | 153 | $count++; 154 | } 155 | } 156 | } 157 | 158 | if (count($result) < $limit) { 159 | break; 160 | } 161 | 162 | if ($limit <= 0) { 163 | break; 164 | } 165 | } 166 | 167 | return $objects; 168 | } 169 | 170 | /** 171 | * @param string $candidate 172 | * 173 | * @return array Object information, for example: 174 | * - id (numeric indentifier as string) 175 | * - title (string) 176 | * 177 | * @throws Exception when object not found 178 | */ 179 | public function identifyObject(string $candidate): array { 180 | if (is_numeric($candidate) && (int) $candidate > 0) { 181 | $object = $this->idoitAPIFactory->getCMDBObject()->read((int) $candidate); 182 | 183 | if (count($object) > 0) { 184 | return $object; 185 | } 186 | 187 | throw new BadMethodCallException(sprintf( 188 | 'Object not found by numeric identifier %s', 189 | $candidate 190 | )); 191 | } 192 | 193 | $objects = $this->fetchObjects([ 194 | 'title' => $candidate 195 | ]); 196 | 197 | switch (count($objects)) { 198 | case 0: 199 | throw new BadMethodCallException(sprintf( 200 | 'Object not found by title "%s"', 201 | $candidate 202 | )); 203 | case 1: 204 | $object = end($objects); 205 | return $object; 206 | default: 207 | throw new RuntimeException(sprintf( 208 | 'Object title "%s" is ambiguous', 209 | $candidate 210 | )); 211 | } 212 | } 213 | 214 | /** 215 | * Translate object titles into identifiers 216 | * 217 | * Keep in mind that there could be objects which 218 | * 219 | * - have none of provided titles or 220 | * - have the same titles. 221 | * 222 | * As a result the returned array could have a smaller, bigger or even size as the provided titles array. 223 | * 224 | * @param array $titles Object titles as strings 225 | * 226 | * @return array List of object identifiers 227 | * 228 | * @throws Exception on error 229 | */ 230 | public function fetchObjectIDsByTitles(array $titles): array { 231 | $objectdIDs = []; 232 | 233 | switch (count($titles)) { 234 | case 0: 235 | throw new BadMethodCallException('Empty list of object titles'); 236 | case 1: 237 | $title = end($titles); 238 | 239 | $objects = $this->idoitAPIFactory->getCMDBObjects()->read([ 240 | 'title' => $title 241 | ]); 242 | 243 | foreach ($objects as $object) { 244 | $objectdIDs[] = (int) $object['id']; 245 | } 246 | break; 247 | default: 248 | $requests = []; 249 | 250 | foreach ($titles as $title) { 251 | $requests[] = [ 252 | 'method' => 'cmdb.objects.read', 253 | 'params' => [ 254 | 'filter' => [ 255 | 'title' => $title 256 | ] 257 | ] 258 | ]; 259 | } 260 | 261 | $result = $this->idoitAPIFactory->getAPI()->batchRequest($requests); 262 | 263 | foreach ($result as $objects) { 264 | foreach ($objects as $object) { 265 | $objectdIDs[] = (int) $object['id']; 266 | } 267 | } 268 | break; 269 | } 270 | 271 | return $objectdIDs; 272 | } 273 | 274 | } 275 | -------------------------------------------------------------------------------- /src/Service/IdoitAPIFactory.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | use \Exception; 30 | use \RuntimeException; 31 | use bheisig\cli\Log; 32 | use bheisig\idoitapi\API; 33 | use bheisig\idoitapi\Request; 34 | use bheisig\idoitapi\CMDBCategory; 35 | use bheisig\idoitapi\CMDBCategoryInfo; 36 | use bheisig\idoitapi\CMDBDialog; 37 | use \bheisig\idoitapi\CMDBLogbook; 38 | use bheisig\idoitapi\CMDBObject; 39 | use bheisig\idoitapi\CMDBObjects; 40 | use bheisig\idoitapi\CMDBObjectTypeCategories; 41 | use bheisig\idoitapi\CMDBObjectTypes; 42 | use bheisig\idoitapi\Idoit; 43 | use bheisig\idoitapi\Subnet; 44 | 45 | /** 46 | * i-doit API factory 47 | */ 48 | class IdoitAPIFactory extends Service { 49 | 50 | /** 51 | * API 52 | * 53 | * @var API 54 | */ 55 | protected $api; 56 | 57 | /** 58 | * Factory 59 | * 60 | * @var array 61 | */ 62 | protected $instances = []; 63 | 64 | /** 65 | * Constructor 66 | * 67 | * @param array $config Configuration settings 68 | * @param Log $log Logger 69 | * 70 | * @throws Exception when configuration settings are missing 71 | */ 72 | public function __construct(array $config, Log $log) { 73 | parent::__construct($config, $log); 74 | 75 | try { 76 | $this->api = new API($this->config['api']); 77 | if (array_key_exists(API::USERNAME, $this->config['api']) && 78 | array_key_exists(API::PASSWORD, $this->config['api'])) { 79 | $this->api->login(); 80 | } 81 | } catch (Exception $e) { 82 | throw new Exception( 83 | 'No proper configuration for i-doit API calls: ' . $e->getMessage() 84 | ); 85 | } 86 | } 87 | 88 | /** 89 | * Get API instance 90 | * 91 | * @return API 92 | */ 93 | public function getAPI(): API { 94 | return $this->api; 95 | } 96 | 97 | /** 98 | * Factory 99 | * 100 | * @param string $class Class name 101 | * 102 | * @return mixed 103 | */ 104 | protected function getInstanceOf($class) { 105 | if (!array_key_exists($class, $this->instances)) { 106 | if (is_a($class, Request::class, true) === false) { 107 | throw new RuntimeException(sprintf( 108 | '%s is not an API request class', 109 | $class 110 | )); 111 | } 112 | 113 | $this->instances[$class] = new $class($this->api); 114 | } 115 | 116 | return $this->instances[$class]; 117 | } 118 | 119 | /** 120 | * Factory for CMDB 121 | * 122 | * @return Idoit 123 | */ 124 | public function getCMDB(): Idoit { 125 | return $this->getInstanceOf(Idoit::class); 126 | } 127 | 128 | /** 129 | * Factory for CMDBObject 130 | * 131 | * @return CMDBObject 132 | */ 133 | public function getCMDBObject(): CMDBObject { 134 | return $this->getInstanceOf(CMDBObject::class); 135 | } 136 | 137 | /** 138 | * Factory for CMDBObjects 139 | * 140 | * @return CMDBObjects 141 | */ 142 | public function getCMDBObjects(): CMDBObjects { 143 | return $this->getInstanceOf(CMDBObjects::class); 144 | } 145 | 146 | /** 147 | * Factory for CMDBObjectTypeCategories 148 | * 149 | * @return CMDBObjectTypeCategories 150 | */ 151 | public function getCMDBObjectTypeCategories(): CMDBObjectTypeCategories { 152 | return $this->getInstanceOf(CMDBObjectTypeCategories::class); 153 | } 154 | 155 | /** 156 | * Factory for CMDBObjectTypes 157 | * 158 | * @return CMDBObjectTypes 159 | */ 160 | public function getCMDBObjectTypes(): CMDBObjectTypes { 161 | return $this->getInstanceOf(CMDBObjectTypes::class); 162 | } 163 | 164 | /** 165 | * Factory for CMDBCategory 166 | * 167 | * @return CMDBCategory 168 | */ 169 | public function getCMDBCategory(): CMDBCategory { 170 | return $this->getInstanceOf(CMDBCategory::class); 171 | } 172 | 173 | /** 174 | * Factory for CMDBCategoryInfo 175 | * 176 | * @return CMDBCategoryInfo 177 | */ 178 | public function getCMDBCategoryInfo(): CMDBCategoryInfo { 179 | return $this->getInstanceOf(CMDBCategoryInfo::class); 180 | } 181 | 182 | /** 183 | * Factory for CMDBDialog 184 | * 185 | * @return CMDBDialog 186 | */ 187 | public function getCMDBDialog(): CMDBDialog { 188 | return $this->getInstanceOf(CMDBDialog::class); 189 | } 190 | 191 | /** 192 | * Factory for CMDBDialog 193 | * 194 | * @return CMDBLogbook 195 | */ 196 | public function getCMDBLogbook(): CMDBLogbook { 197 | return $this->getInstanceOf(CMDBLogbook::class); 198 | } 199 | 200 | /** 201 | * Factory for Subnet 202 | * 203 | * @return Subnet 204 | */ 205 | public function getSubnet(): Subnet { 206 | return $this->getInstanceOf(Subnet::class); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/Service/IdoitStatus.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | /** 30 | * i-doit status 31 | */ 32 | class IdoitStatus extends Service { 33 | 34 | const STATUS_NORMAL = 2; 35 | const STATUS_ARCHIVED = 3; 36 | const STATUS_DELETED = 4; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Service/PrintData.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | use \Exception; 30 | use \BadMethodCallException; 31 | use \RuntimeException; 32 | use bheisig\cli\Log; 33 | 34 | /** 35 | * Print data 36 | */ 37 | class PrintData extends Service { 38 | 39 | /** 40 | * Offset 41 | * 42 | * @var int Defaults to 0 43 | */ 44 | protected $offset = 0; 45 | 46 | /** 47 | * i-doit API 48 | * 49 | * @var IdoitAPI 50 | */ 51 | protected $idoitAPI; 52 | 53 | /** 54 | * i-doit API factory 55 | * 56 | * @var IdoitAPIFactory 57 | */ 58 | protected $idoitAPIFactory; 59 | 60 | /** 61 | * Set offset 62 | * 63 | * @param int $offset Offset 64 | * 65 | * @return self Returns itself 66 | * 67 | * @throws BadMethodCallException on invalid parameter 68 | */ 69 | public function setOffset(int $offset): self { 70 | if ($offset < 0) { 71 | throw new BadMethodCallException('Offset must be greater or equal zero'); 72 | } 73 | 74 | $this->offset = $offset; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Print category entry 81 | * 82 | * @param array $entry Entry with key-value pairs of attribute/value 83 | * @param array $attributeDefinitions Attribute descriptions provided by API method "cmdb.category.info" 84 | * @param HandleAttribute $handleAttribute Attribute service 85 | * @param int $level Log level; defaults to "info" 86 | * @param string $printAs Print log as output (default) or as message 87 | * 88 | * @return self Returns itself 89 | * 90 | * @throws Exception on error 91 | */ 92 | public function printEntry( 93 | array $entry, 94 | array $attributeDefinitions, 95 | HandleAttribute $handleAttribute, 96 | int $level = Log::INFO, 97 | string $printAs = Log::PRINT_AS_OUTPUT 98 | ): self { 99 | if (count($entry) === 0) { 100 | throw new BadMethodCallException('Entry is empty'); 101 | } 102 | 103 | if (count($attributeDefinitions) === 0) { 104 | throw new BadMethodCallException('Empty attribute definitions'); 105 | } 106 | 107 | foreach ($entry as $attribute => $value) { 108 | if ($attribute === 'id' || $attribute === 'objID') { 109 | continue; 110 | } 111 | 112 | if (!array_key_exists($attribute, $attributeDefinitions)) { 113 | continue; 114 | } 115 | 116 | $handleAttribute->load($attributeDefinitions[$attribute]); 117 | 118 | if ($handleAttribute->ignore() || $handleAttribute->isReadonly()) { 119 | continue; 120 | } 121 | 122 | $encodedValue = $handleAttribute->encode($value); 123 | 124 | if (strlen($encodedValue) === 0) { 125 | $encodedValue = '–'; 126 | } 127 | 128 | if (!array_key_exists('title', $attributeDefinitions[$attribute])) { 129 | throw new RuntimeException(sprintf( 130 | 'No localized title found for attribute "%s"', 131 | $attribute 132 | )); 133 | } 134 | 135 | $attributeTitle = $attributeDefinitions[$attribute]['title']; 136 | 137 | switch ($printAs) { 138 | case Log::PRINT_AS_OUTPUT: 139 | $this->log->printAsOutput(); 140 | break; 141 | case Log::PRINT_AS_MESSAGE: 142 | $this->log->printAsMessage(); 143 | break; 144 | } 145 | 146 | $this->log->event( 147 | $level, 148 | '%s%s: %s', 149 | str_repeat(' ', $this->offset), 150 | $attributeTitle, 151 | $encodedValue 152 | ); 153 | } 154 | 155 | return $this; 156 | } 157 | 158 | public function printDialogEntries( 159 | array $entries, 160 | int $level = Log::INFO, 161 | string $printAs = Log::PRINT_AS_OUTPUT 162 | ): self { 163 | switch ($printAs) { 164 | case Log::PRINT_AS_OUTPUT: 165 | $this->log->printAsOutput(); 166 | break; 167 | case Log::PRINT_AS_MESSAGE: 168 | $this->log->printAsMessage(); 169 | break; 170 | } 171 | 172 | foreach ($entries as $entry) { 173 | $this->log->event( 174 | $level, 175 | '%s%s [%s]', 176 | str_repeat(' ', $this->offset), 177 | $entry['title'], 178 | $entry['id'] 179 | ); 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/Service/Service.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | use bheisig\cli\Log; 30 | 31 | /** 32 | * Base class for services 33 | */ 34 | abstract class Service { 35 | 36 | /** 37 | * Configuration settings as key-value store 38 | * 39 | * @var array Associative array 40 | */ 41 | protected $config = []; 42 | 43 | /** 44 | * Logger 45 | * 46 | * @var Log 47 | */ 48 | protected $log; 49 | 50 | /** 51 | * Constructor 52 | * 53 | * @param array $config Configuration settings 54 | * @param Log $log Logger 55 | */ 56 | public function __construct(array $config, Log $log) { 57 | $this->config = $config; 58 | $this->log = $log; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/Service/Validate.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @author Benjamin Heisig 20 | * @copyright Copyright (C) 2016-17 Benjamin Heisig 21 | * @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 22 | * @link https://github.com/bheisig/i-doit-cli 23 | */ 24 | 25 | declare(strict_types=1); 26 | 27 | namespace bheisig\idoitcli\Service; 28 | 29 | /** 30 | * Validate data 31 | */ 32 | class Validate extends Service { 33 | 34 | public function isOneLiner(string $value): bool { 35 | $length = strlen($value); 36 | 37 | if ($length <= 0) { 38 | return false; 39 | } 40 | 41 | if ($length > HandleAttribute::SHORT_TEXT_LENGTH) { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | public function isIDAsString(string $id) { 49 | return is_numeric($id) && (int) $id > 0; 50 | } 51 | 52 | public function isID(int $id): bool { 53 | return $id > 0; 54 | } 55 | 56 | } 57 | --------------------------------------------------------------------------------