diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:43:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:43:12 +0000 |
commit | cd989f9c3aff968e19a3aeabc4eb9085787a6673 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-upstream.tar.xz icingaweb2-module-director-upstream.zip |
Adding upstream version 1.10.2.upstream/1.10.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
1129 files changed, 137634 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..e9bddce --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,24 @@ +## Expected Behavior +<!--- If you're describing a bug, tell us what should happen --> +<!--- If you're suggesting a change/improvement, tell us how it should work --> + +## Current Behavior +<!--- If describing a bug, tell us what happens instead of the expected behavior --> +<!--- If suggesting a change/improvement, explain the difference from current behavior --> + +## Possible Solution +<!--- Not obligatory, but suggest a fix/reason for the bug, --> +<!--- or ideas how to implement: the addition or change --> + +## Steps to Reproduce (for bugs) +<!--- Provide a link to a live example, or an unambiguous set of steps to --> +<!--- reproduce this bug. Include configuration, logs, etc. to reproduce, if relevant --> + +## Your Environment +<!--- Include as many relevant details about the environment you experienced the problem in --> +* Director version (System - About): +* Icinga Web 2 version and modules (System - About): +* Icinga 2 version (`icinga2 --version`): +* Operating System and version: +* Webserver, PHP versions: + diff --git a/.github/workflows/L10n-update.yml b/.github/workflows/L10n-update.yml new file mode 100644 index 0000000..9dce59a --- /dev/null +++ b/.github/workflows/L10n-update.yml @@ -0,0 +1,20 @@ +name: L10n Update + +on: + push: + branches: + - master + +jobs: + trigger-update: + name: L10n Update Trigger + runs-on: ubuntu-latest + + steps: + - name: Repository dispatch + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.ICINGABOT_TOKEN }} + repository: Icinga/L10n + event-type: update + client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3511072 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +## Editors +/.idea/ +.*.sw[op] + +## PHP vendor artifacts +/vendor/ @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6af13c --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +Icinga Director +=============== + +Icinga Director has been designed to make Icinga 2 configuration handling easy. +It tries to target two main audiences: + +* Users with the desire to completely automate their datacenter +* Sysops willing to grant their "point & click" users a lot of flexibility + +What makes Icinga Director so special is the fact that it tries to target both +of them at once. + + + +Read more about Icinga Director in our [Introduction](doc/01-Introduction.md) section. +Afterwards, you should be ready for [getting started](doc/04-Getting-started.md). + +Documentation +------------- + +Please have a look at our [Installation instructions](doc/02-Installation.md) +and our hints for how to apply [Upgrades](doc/05-Upgrading.md). We love automation +and in case you also do so, the [Automation chapter](doc/03-Automation.md) could +be worth a read. When upgrading, you should also have a look at our [Changelog](doc/82-Changelog.md). + +You could be interested in understanding how the [Director works](doc/10-How-it-works.md) +internally. [Working with agents](doc/24-Working-with-agents.md) is a topic that +affects many Icinga administrators. Other interesting entry points might be +[Import and Synchronization](doc/70-Import-and-Sync.md), our [CLI interface](doc/60-CLI.md), +the [REST API](doc/70-REST-API.md) and last but not least our [FAQ](doc/80-FAQ.md). + +A complete list of all our documentation can be found in the [doc](doc/) directory. + +Contributing +------------ + +Icinga Director is an Open Source project and lives from your contributions. No +matter whether these are feature requests, issues, translations, documentation +or code. + +* Please check whether a related issue already exists on our [Issue Tracker](https://github.com/Icinga/icingaweb2-module-director/issues) +* Make sure your code conforms to the [PSR-2: Coding Style Guide](http://www.php-fig.org/psr/psr-2/) +* [Unit-Tests](doc/93-Testing.md) would be great +* Send a [Pull Request](https://github.com/Icinga/icingaweb2-module-director/pulls) + +Addons +------ + +The following are to be considered community-supported modules, as they are not +supported by the Icinga Team. At least not yet. But please give them a try if +they fit your needs. They are being used in productive environments: + +* [AWS - Amazon Web Services](https://github.com/Icinga/icingaweb2-module-aws): + provides an Import Source for Autoscaling Groups on AWS +* [File-Shipper](https://github.com/Icinga/icingaweb2-module-fileshipper): + allows Director to ship additional config files with manual config with its + deployments +* [PuppetDB](https://github.com/Icinga/icingaweb2-module-puppetdb): provides + an Import Source dealing with your PuppetDB +* [vSphere](https://github.com/Icinga/icingaweb2-module-vsphere): VMware vSphere + Import Source for Virtual Machines and Host Systems diff --git a/application/clicommands/BasketCommand.php b/application/clicommands/BasketCommand.php new file mode 100644 index 0000000..dd2434f --- /dev/null +++ b/application/clicommands/BasketCommand.php @@ -0,0 +1,127 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot; +use Icinga\Module\Director\DirectorObject\ObjectPurgeHelper; + +/** + * Export Director Config Objects + */ +class BasketCommand extends Command +{ + /** + * List configured Baskets + * + * USAGE + * + * icingacli director basket list + * + * OPTIONS + */ + public function listAction() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('director_basket', 'basket_name') + ->order('basket_name'); + foreach ($db->fetchCol($query) as $name) { + echo "$name\n"; + } + } + + /** + * JSON-dump for objects related to the given Basket + * + * USAGE + * + * icingacli director basket dump --name <basket> + * + * OPTIONS + */ + public function dumpAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::createForBasket($basket, $this->db()); + echo $snapshot->getJsonDump() . "\n"; + } + + /** + * Take a snapshot for the given Basket + * + * USAGE + * + * icingacli director basket snapshot --name <basket> + * + * OPTIONS + */ + public function snapshotAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::createForBasket($basket, $this->db()); + $snapshot->store(); + $hexSum = bin2hex($snapshot->get('content_checksum')); + printf( + "Snapshot '%s' taken for Basket '%s' at %s\n", + substr($hexSum, 0, 7), + $basket->get('basket_name'), + DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000) + ); + } + + /** + * Restore a Basket from JSON dump provided on STDIN + * + * USAGE + * + * icingacli director basket restore < basket-dump.json + * + * OPTIONS + * --purge <ObjectType>[,<ObjectType] Purge objects of the + * Given types. WARNING: this removes ALL objects that are + * not shipped with the given basket + * --force Purge refuses to purge Objects in case there are + * no Objects of a given ObjectType in the provided basket + * unless forced to do so + */ + public function restoreAction() + { + if ($purge = $this->params->get('purge')) { + $purge = explode(',', $purge); + ObjectPurgeHelper::assertObjectTypesAreEligibleForPurge($purge); + } + $json = file_get_contents('php://stdin'); + BasketSnapshot::restoreJson($json, $this->db()); + if ($purge) { + $this->purgeObjectTypes(Json::decode($json), $purge, $this->params->get('force')); + } + echo "Objects from Basket Snapshot have been restored\n"; + } + + protected function purgeObjectTypes($objects, array $types, $force = false) + { + $helper = new ObjectPurgeHelper($this->db()); + if ($force) { + $helper->force(); + } + foreach ($types as $type) { + list($className, $typeFilter) = BasketSnapshot::getClassAndObjectTypeForType($type); + $helper->purge( + isset($objects->$type) ? (array) $objects->$type : [], + $className, + $typeFilter + ); + } + } + + /** + */ + protected function requireBasket() + { + return Basket::load($this->params->getRequired('name'), $this->db()); + } +} diff --git a/application/clicommands/BenchmarkCommand.php b/application/clicommands/BenchmarkCommand.php new file mode 100644 index 0000000..6ccd8c8 --- /dev/null +++ b/application/clicommands/BenchmarkCommand.php @@ -0,0 +1,152 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\CustomVariable\CustomVariable; +use Icinga\Module\Director\Data\Db\IcingaObjectFilterRenderer; +use Icinga\Module\Director\Data\Db\IcingaObjectQuery; +use Icinga\Module\Director\Objects\HostGroupMembershipResolver; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaHostVar; +use Icinga\Module\Director\Objects\IcingaVar; + +class BenchmarkCommand extends Command +{ + public function testflatfilterAction() + { + $q = new IcingaObjectQuery('host', $this->db()); + $filter = Filter::fromQueryString( + // 'host.vars.snmp_community="*ub*"&(host.vars.location="London"|host.vars.location="Berlin")' + // 'host.vars.snmp_community="*ub*"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")' + 'host.vars.priority="*igh"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")' + ); + IcingaObjectFilterRenderer::apply($filter, $q); + echo $q->getSql() . "\n"; + + print_r($q->listNames()); + } + + public function rerendervarsAction() + { + $conn = $this->db(); + $db = $conn->getDbAdapter(); + $db->beginTransaction(); + $query = $db->select()->from( + array('v' => 'icinga_var'), + array( + 'v.varname', + 'v.varvalue', + 'v.checksum', + 'v.rendered_checksum', + 'v.rendered', + 'format' => "('json')", + ) + ); + Benchmark::measure('Ready to fetch all vars'); + $rows = $db->fetchAll($query); + Benchmark::measure('Got vars, storing flat'); + foreach ($rows as $row) { + $var = CustomVariable::fromDbRow($row); + $rendered = $var->render(); + $checksum = sha1($rendered, true); + if ($checksum === $row->rendered_checksum) { + continue; + } + + $where = $db->quoteInto('checksum = ?', $row->checksum); + $db->update( + 'icinga_var', + array( + 'rendered' => $rendered, + 'rendered_checksum' => $checksum + ), + $where + ); + } + + $db->commit(); + } + + public function flattenvarsAction() + { + $conn = $this->db(); + $db = $conn->getDbAdapter(); + $db->beginTransaction(); + $query = $db->select()->from(['v' => 'icinga_host_var'], [ + 'v.host_id', + 'v.varname', + 'v.varvalue', + 'v.format', + 'v.checksum' + ]); + Benchmark::measure('Ready to fetch all vars'); + $rows = $db->fetchAll($query); + Benchmark::measure('Got vars, storing flat'); + + foreach ($rows as $row) { + $var = CustomVariable::fromDbRow($row); + $checksum = $var->checksum(); + if (! IcingaVar::exists($checksum, $conn)) { + IcingaVar::generateForCustomVar($var, $conn); + } + + if ($row->checksum === null) { + $where = $db->quoteInto('host_id = ?', $row->host_id) + . $db->quoteInto(' AND varname = ?', $row->varname); + $db->update('icinga_host_var', ['checksum' => $checksum], $where); + } + } + + $db->commit(); + } + + public function resolvehostgroupsAction() + { + $resolver = new HostGroupMembershipResolver($this->db()); + $resolver->refreshDb(); + } + + public function filterAction() + { + $flat = []; + + /** @var FilterChain|FilterExpression $filter */ + $filter = Filter::fromQueryString( + // 'object_name=*ic*2*&object_type=object' + 'vars.bpconfig=*' + ); + Benchmark::measure('ready'); + $objs = IcingaHost::loadAll($this->db()); + Benchmark::measure('db done'); + + foreach ($objs as $host) { + $flat[$host->get('id')] = (object) []; + foreach ($host->getProperties() as $k => $v) { + $flat[$host->get('id')]->$k = $v; + } + } + Benchmark::measure('objects ready'); + + $vars = IcingaHostVar::loadAll($this->db()); + Benchmark::measure('vars loaded'); + foreach ($vars as $var) { + if (! array_key_exists($var->get('host_id'), $flat)) { + // Templates? + continue; + } + $flat[$var->get('host_id')]->{'vars.' . $var->get('varname')} = $var->get('varvalue'); + } + Benchmark::measure('vars done'); + + foreach ($flat as $host) { + if ($filter->matches($host)) { + echo $host->object_name . "\n"; + } + } + } +} diff --git a/application/clicommands/CommandCommand.php b/application/clicommands/CommandCommand.php new file mode 100644 index 0000000..5c96442 --- /dev/null +++ b/application/clicommands/CommandCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Commands + * + * Use this command to show, create, modify or delete Icinga Command + * objects + */ +class CommandCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/CommandsCommand.php b/application/clicommands/CommandsCommand.php new file mode 100644 index 0000000..9a74337 --- /dev/null +++ b/application/clicommands/CommandsCommand.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectsCommand; + +/** + * List Icinga Commands + * + * Use this command to list Icinga Command objects + */ +class CommandsCommand extends ObjectsCommand +{ +} diff --git a/application/clicommands/ConfigCommand.php b/application/clicommands/ConfigCommand.php new file mode 100644 index 0000000..e313aa4 --- /dev/null +++ b/application/clicommands/ConfigCommand.php @@ -0,0 +1,178 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Application\Benchmark; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Deployment\ConditionalDeployment; +use Icinga\Module\Director\Deployment\DeploymentGracePeriod; +use Icinga\Module\Director\Deployment\DeploymentStatus; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Import\SyncUtils; + +/** + * Generate, show and deploy Icinga 2 configuration + */ +class ConfigCommand extends Command +{ + /** + * Re-render the current configuration + */ + public function renderAction() + { + $profile = $this->params->shift('profile'); + if ($profile) { + $this->enableDbProfiler(); + } + + $config = new IcingaConfig($this->db()); + Benchmark::measure('Rendering config'); + if ($config->hasBeenModified()) { + Benchmark::measure('Config rendered, storing to db'); + $config->store(); + Benchmark::measure('All done'); + $checksum = $config->getHexChecksum(); + printf( + "New config with checksum %s has been generated\n", + $checksum + ); + } else { + $checksum = $config->getHexChecksum(); + printf( + "Config with checksum %s already exists\n", + $checksum + ); + } + + if ($profile) { + $this->dumpDbProfile(); + } + } + + protected function dumpDbProfile() + { + $profiler = $this->getDbProfiler(); + + $totalTime = $profiler->getTotalElapsedSecs(); + $queryCount = $profiler->getTotalNumQueries(); + $longestTime = 0; + $longestQuery = null; + + /** @var \Zend_Db_Profiler_Query $query */ + foreach ($profiler->getQueryProfiles() as $query) { + echo $query->getQuery() . "\n"; + if ($query->getElapsedSecs() > $longestTime) { + $longestTime = $query->getElapsedSecs(); + $longestQuery = $query->getQuery(); + } + } + + echo 'Executed ' . $queryCount . ' queries in ' . $totalTime . ' seconds' . "\n"; + echo 'Average query length: ' . $totalTime / $queryCount . ' seconds' . "\n"; + echo 'Queries per second: ' . $queryCount / $totalTime . "\n"; + echo 'Longest query length: ' . $longestTime . "\n"; + echo "Longest query: \n" . $longestQuery . "\n"; + } + + protected function getDbProfiler() + { + return $this->db()->getDbAdapter()->getProfiler(); + } + + protected function enableDbProfiler() + { + return $this->getDbProfiler()->setEnabled(true); + } + + /** + * Deploy the current configuration + * + * USAGE + * + * icingacli director config deploy [--checksum <checksum>] [--force] [--wait <seconds>] + * [--grace-period <seconds>] + * + * OPTIONS + * + * --checksum <checksum> Optionally deploy a specific configuration + * --force Force a deployment, even when the configuration + * hasn't changed + * --wait <seconds> Optionally wait until Icinga completed it's + * restart + * --grace-period <seconds> Do not deploy if a deployment took place + * less than <seconds> ago + */ + public function deployAction() + { + $db = $this->db(); + + $checksum = $this->params->get('checksum'); + if ($checksum) { + $config = IcingaConfig::load(hex2bin($checksum), $db); + } else { + $config = IcingaConfig::generate($db); + $checksum = $config->getHexChecksum(); + } + + $deployer = new ConditionalDeployment($db, $this->api()); + $deployer->force((bool) $this->params->get('force')); + if ($graceTime = $this->params->get('grace-period')) { + $deployer->setGracePeriod(new DeploymentGracePeriod((int) $graceTime, $db)); + if ($this->params->get('force')) { + fwrite(STDERR, "WARNING: force overrides Grace period\n"); + } + } + $deployer->refresh(); + + if ($deployment = $deployer->deploy($config)) { + if ($deployer->hasBeenForced()) { + echo $deployer->getNoDeploymentReason() . ", deploying anyway\n"; + } + printf("Config '%s' has been deployed\n", $checksum); + } else { + echo $deployer->getNoDeploymentReason() . "\n"; + return; + } + + if ($timeout = $this->getWaitTime()) { + $deployed = $deployer->waitForStartupAfterDeploy($deployment, $timeout); + if ($deployed !== true) { + $this->fail("Waiting for Icinga restart failed '%s': %s\n", $checksum, $deployed); + } + } + } + + /** + * Checks the deployments status + */ + public function deploymentstatusAction() + { + $db = $this->db(); + $api = $this->api(); + $status = new DeploymentStatus($db, $api); + $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities')); + if ($key = $this->params->get('key')) { + $result = SyncUtils::getSpecificValue($result, $key); + } + + if (is_string($result)) { + echo "$result\n"; + } else { + echo Json::encode($result, JSON_PRETTY_PRINT) . "\n"; + } + } + + protected function getWaitTime() + { + if ($timeout = $this->params->get('wait')) { + if (!ctype_digit($timeout)) { + $this->fail("--wait must be the number of seconds to wait'"); + } + + return (int) $timeout; + } + + return null; + } +} diff --git a/application/clicommands/CoreCommand.php b/application/clicommands/CoreCommand.php new file mode 100644 index 0000000..4927aa5 --- /dev/null +++ b/application/clicommands/CoreCommand.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\PlainObjectRenderer; + +class CoreCommand extends Command +{ + public function constantsAction() + { + foreach ($this->api()->getConstants() as $name => $value) { + printf("const %s = %s\n", $name, PlainObjectRenderer::render($value)); + } + } +} diff --git a/application/clicommands/DaemonCommand.php b/application/clicommands/DaemonCommand.php new file mode 100644 index 0000000..e89e1da --- /dev/null +++ b/application/clicommands/DaemonCommand.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Daemon\BackgroundDaemon; + +class DaemonCommand extends Command +{ + /** + * Run the main Director daemon + * + * USAGE + * + * icingacli director daemon run [--db-resource <name>] + */ + public function runAction() + { + $this->app->getModuleManager()->loadEnabledModules(); + $daemon = new BackgroundDaemon(); + if ($dbResource = $this->params->get('db-resource')) { + $daemon->setDbResourceName($dbResource); + } + $daemon->run(); + } +} diff --git a/application/clicommands/DependencyCommand.php b/application/clicommands/DependencyCommand.php new file mode 100644 index 0000000..ff5cbdc --- /dev/null +++ b/application/clicommands/DependencyCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Dependencies + * + * Use this command to show, create, modify or delete Icinga Dependency + * objects + */ +class DependencyCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/EndpointCommand.php b/application/clicommands/EndpointCommand.php new file mode 100644 index 0000000..f61f4fc --- /dev/null +++ b/application/clicommands/EndpointCommand.php @@ -0,0 +1,19 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Endpoints + * + * Use this command to show, create, modify or delete Icinga Endpoint + * objects + */ +class EndpointCommand extends ObjectCommand +{ + public function statusAction() + { + print_r($this->api()->getStatus()); + } +} diff --git a/application/clicommands/ExportCommand.php b/application/clicommands/ExportCommand.php new file mode 100644 index 0000000..2b2119d --- /dev/null +++ b/application/clicommands/ExportCommand.php @@ -0,0 +1,180 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\DirectorObject\Automation\ImportExport; + +/** + * Export Director Config Objects + */ +class ExportCommand extends Command +{ + /** + * Export all ImportSource definitions + * + * USAGE + * + * icingacli director export importsources [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function importsourcesAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllImportSources(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all SyncRule definitions + * + * USAGE + * + * icingacli director export syncrules [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function syncrulesAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllSyncRules(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all Job definitions + * + * USAGE + * + * icingacli director export jobs [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function jobsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllJobs(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all DataField definitions + * + * USAGE + * + * icingacli director export datafields [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function datafieldsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllDataFields(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all DataList definitions + * + * USAGE + * + * icingacli director export datalists [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function datalistsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllDataLists(), + !$this->params->shift('no-pretty') + ); + } + + // /** + // * Export all IcingaHostGroup definitions + // * + // * USAGE + // * + // * icingacli director export hostgroup [options] + // * + // * OPTIONS + // * + // * --no-pretty JSON is pretty-printed per default + // * Use this flag to enforce unformatted JSON + // */ + // public function hostgroupAction() + // { + // $export = new ImportExport($this->db()); + // echo $this->renderJson( + // $export->serializeAllHostGroups(), + // !$this->params->shift('no-pretty') + // ); + // } + // + // /** + // * Export all IcingaServiceGroup definitions + // * + // * USAGE + // * + // * icingacli director export servicegroup [options] + // * + // * OPTIONS + // * + // * --no-pretty JSON is pretty-printed per default + // * Use this flag to enforce unformatted JSON + // */ + // public function servicegroupAction() + // { + // $export = new ImportExport($this->db()); + // echo $this->renderJson( + // $export->serializeAllServiceGroups(), + // !$this->params->shift('no-pretty') + // ); + // } + + /** + * Export all IcingaTemplateChoiceHost definitions + * + * USAGE + * + * icingacli director export hosttemplatechoices [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function hosttemplatechoicesAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllHostTemplateChoices(), + !$this->params->shift('no-pretty') + ); + } +} diff --git a/application/clicommands/HealthCommand.php b/application/clicommands/HealthCommand.php new file mode 100644 index 0000000..1635c50 --- /dev/null +++ b/application/clicommands/HealthCommand.php @@ -0,0 +1,80 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\CheckPlugin\PluginState; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Health; +use Icinga\Module\Director\Cli\PluginOutputBeautifier; + +/** + * Check Icinga Director Health + * + * Use this command as a CheckPlugin to monitor your Icinga Director health + */ +class HealthCommand extends Command +{ + /** + * Run health checks + * + * Use this command to run all or a specific set of Health Checks. + * + * USAGE + * + * icingacli director health check [options] + * + * OPTIONS + * + * --check <name> Run only a specific set of checks + * valid names: config, sync, import, jobs, deployment + * --db <name> Use a specific Icinga Web DB resource + * --watch <seconds> Refresh every <second>. For interactive use only + */ + public function checkAction() + { + $health = new Health(); + if ($name = $this->params->get('db')) { + $health->setDbResourceName($name); + } + + if ($name = $this->params->get('check')) { + $check = $health->getCheck($name); + echo PluginOutputBeautifier::beautify($check->getOutput(), $this->screen); + + exit($check->getState()->getNumeric()); + } else { + $state = new PluginState('OK'); + $checks = $health->getAllChecks(); + + $output = []; + foreach ($checks as $check) { + $state->raise($check->getState()); + $output[] = $check->getOutput(); + } + + if ($state->getNumeric() === 0) { + echo "Icinga Director: everything is fine\n\n"; + } else { + echo "Icinga Director: there are problems\n\n"; + } + + $out = PluginOutputBeautifier::beautify(implode("\n", $output), $this->screen); + echo $out; + + if (! $this->isBeingWatched()) { + exit($state->getNumeric()); + } + } + } + + /** + * Cli should provide this information, as it shifts the parameter + * + * @return bool + */ + protected function isBeingWatched() + { + global $argv; + return in_array('--watch', $argv); + } +} diff --git a/application/clicommands/HostCommand.php b/application/clicommands/HostCommand.php new file mode 100644 index 0000000..21ec5eb --- /dev/null +++ b/application/clicommands/HostCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Hosts + * + * Use this command to show, create, modify or delete Icinga Host + * objects + */ +class HostCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/HostgroupCommand.php b/application/clicommands/HostgroupCommand.php new file mode 100644 index 0000000..88b17d9 --- /dev/null +++ b/application/clicommands/HostgroupCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Hostgroups + * + * Use this command to show, create, modify or delete Icinga Hostgroups + * objects + */ +class HostGroupCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/HostgroupsCommand.php b/application/clicommands/HostgroupsCommand.php new file mode 100644 index 0000000..1007a05 --- /dev/null +++ b/application/clicommands/HostgroupsCommand.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectsCommand; + +/** + * Manage Icinga Hostgroups + * + * Use this command to list Icinga Hostgroup objects + */ +class HostgroupsCommand extends ObjectsCommand +{ +} diff --git a/application/clicommands/HostsCommand.php b/application/clicommands/HostsCommand.php new file mode 100644 index 0000000..3008284 --- /dev/null +++ b/application/clicommands/HostsCommand.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectsCommand; + +/** + * Manage Icinga Hosts + * + * Use this command to list Icinga Host objects + */ +class HostsCommand extends ObjectsCommand +{ +} diff --git a/application/clicommands/HousekeepingCommand.php b/application/clicommands/HousekeepingCommand.php new file mode 100644 index 0000000..974e28d --- /dev/null +++ b/application/clicommands/HousekeepingCommand.php @@ -0,0 +1,74 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Exception\MissingParameterException; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Db\Housekeeping; +use Icinga\Module\Director\Db\MembershipHousekeeping; + +class HousekeepingCommand extends Command +{ + protected $housekeeping; + + public function tasksAction() + { + if ($pending = $this->params->shift('pending')) { + $tasks = $this->housekeeping()->getPendingTaskSummary(); + } else { + $tasks = $this->housekeeping()->getTaskSummary(); + } + + $len = array_reduce( + $tasks, + function ($max, $task) { + return max( + $max, + strlen($task->title) + strlen($task->name) + 3 + ); + } + ); + + if (count($tasks)) { + print "\n"; + printf(" %-" . $len . "s | %s\n", 'Housekeeping task (name)', 'Count'); + printf("-%-" . $len . "s-|-------\n", str_repeat('-', $len)); + } + + foreach ($tasks as $task) { + printf( + " %-" . $len . "s | %5d\n", + sprintf('%s (%s)', $task->title, $task->name), + $task->count + ); + } + + if (count($tasks)) { + print "\n"; + } + } + + public function runAction() + { + if (!$job = $this->params->shift()) { + throw new MissingParameterException( + 'Job is required, say ALL to run all pending jobs' + ); + } + + if ($job === 'ALL') { + $this->housekeeping()->runAllTasks(); + } else { + $this->housekeeping()->runTask($job); + } + } + + protected function housekeeping() + { + if ($this->housekeeping === null) { + $this->housekeeping = new Housekeeping($this->db()); + } + + return $this->housekeeping; + } +} diff --git a/application/clicommands/ImportCommand.php b/application/clicommands/ImportCommand.php new file mode 100644 index 0000000..3edfff2 --- /dev/null +++ b/application/clicommands/ImportCommand.php @@ -0,0 +1,62 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\DirectorObject\Automation\ImportExport; +use Icinga\Module\Director\Objects\ImportSource; + +/** + * Export Director Config Objects + */ +class ImportCommand extends Command +{ + /** + * Import ImportSource definitions + * + * USAGE + * + * icingacli director import importsources < importsources.json + * + * OPTIONS + */ + public function importsourcesAction() + { + $json = file_get_contents('php://stdin'); + $import = new ImportExport($this->db()); + $count = $import->unserializeImportSources(json_decode($json)); + echo "$count Import Sources have been imported\n"; + } + + // /** + // * Import an ImportSource definition + // * + // * USAGE + // * + // * icingacli director import importsource < importsource.json + // * + // * OPTIONS + // */ + // public function importsourcection() + // { + // $json = file_get_contents('php://stdin'); + // $object = ImportSource::import(json_decode($json), $this->db()); + // $object->store(); + // printf("Import Source '%s' has been imported\n", $object->getObjectName()); + // } + + /** + * Import SyncRule definitions + * + * USAGE + * + * icingacli director import syncrules < syncrules.json + */ + public function syncrulesAction() + { + $json = file_get_contents('php://stdin'); + $import = new ImportExport($this->db()); + $count = $import->unserializeSyncRules(json_decode($json)); + echo "$count Sync Rules have been imported\n"; + } +} diff --git a/application/clicommands/ImportsourceCommand.php b/application/clicommands/ImportsourceCommand.php new file mode 100644 index 0000000..477fdf5 --- /dev/null +++ b/application/clicommands/ImportsourceCommand.php @@ -0,0 +1,168 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Application\Benchmark; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Objects\ImportSource; + +/** + * Deal with Director Import Sources + * + * Use this command to check or trigger your defined Import Sources + */ +class ImportsourceCommand extends Command +{ + /** + * List defined Import Sources + * + * This shows a table with your defined Import Sources, their IDs and + * current state. As triggering Imports requires an ID, this is where + * you can look up the desired ID. + * + * USAGE + * + * icingacli director importsource list + */ + public function listAction() + { + $sources = ImportSource::loadAll($this->db()); + if (empty($sources)) { + echo "No Import Source has been defined\n"; + + return; + } + + printf("%4s | %s\n", 'ID', 'Import Source name'); + printf("-----+%s\n", str_repeat('-', 64)); + + foreach ($sources as $source) { + $state = $source->get('import_state'); + printf("%4d | %s\n", $source->get('id'), $source->get('source_name')); + printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $source->get('last_error_message') : ''); + } + } + + /** + * Check a given Import Source for changes + * + * This command fetches data from the given Import Source and compares it + * to the most recently imported data. + * + * USAGE + * + * icingacli director importsource check --id <id> + * + * OPTIONS + * + * --id <id> An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function checkAction() + { + $source = $this->getImportSource(); + $source->checkForChanges(); + $this->showImportStateDetails($source); + } + + /** + * Fetch current data from a given Import Source + * + * This command fetches data from the given Import Source and outputs + * them as plain JSON + * + * USAGE + * + * icingacli director importsource fetch --id <id> + * + * OPTIONS + * + * --id <id> An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function fetchAction() + { + $source = $this->getImportSource(); + $source->checkForChanges(); + $hook = ImportSourceHook::forImportSource($source); + Benchmark::measure('Ready to fetch data'); + $data = $hook->fetchData(); + $source->applyModifiers($data); + Benchmark::measure(sprintf('Got %d rows, ready to dump JSON', count($data))); + echo Json::encode($data, JSON_PRETTY_PRINT); + } + + /** + * Trigger an Import Run for a given Import Source + * + * This command fetches data from the given Import Source and stores it to + * the Director DB, so that the next related Sync Rule run can work with + * fresh data. In case data didn't change, nothing is going to be stored. + * + * USAGE + * + * icingacli director importsource run --id <id> + * + * OPTIONS + * + * --id <id> An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function runAction() + { + $source = $this->getImportSource(); + + if ($source->runImport()) { + print "New data has been imported\n"; + $this->showImportStateDetails($source); + } else { + print "Nothing has been changed, imported data is still up to date\n"; + } + } + + /** + * @return ImportSource + */ + protected function getImportSource() + { + return ImportSource::loadWithAutoIncId( + (int) $this->params->getRequired('id'), + $this->db() + ); + } + + /** + * @param ImportSource $source + * @throws \Icinga\Exception\IcingaException + */ + protected function showImportStateDetails(ImportSource $source) + { + echo $this->getImportStateDescription($source) . "\n"; + } + + /** + * @param ImportSource $source + * @return string + * @throws \Icinga\Exception\IcingaException + */ + protected function getImportStateDescription(ImportSource $source) + { + switch ($source->get('import_state')) { + case 'unknown': + return "It's currently unknown whether we are in sync with this" + . ' Import Source. You should either check for changes or' + . ' trigger a new Import Run.'; + case 'in-sync': + return 'This Import Source is in sync'; + case 'pending-changes': + return 'There are pending changes for this Import Source. You' + . ' should trigger a new Import Run.'; + case 'failing': + return 'This Import Source failed: ' . $source->get('last_error_message'); + default: + return 'This Import Source has an invalid state: ' . $source->get('import_state'); + } + } +} diff --git a/application/clicommands/JobsCommand.php b/application/clicommands/JobsCommand.php new file mode 100644 index 0000000..1c6297f --- /dev/null +++ b/application/clicommands/JobsCommand.php @@ -0,0 +1,74 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Exception; +use gipfl\Cli\Process; +use gipfl\Protocol\JsonRpc\Connection; +use gipfl\Protocol\NetString\StreamWrapper; +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Daemon\JsonRpcLogWriter as JsonRpcLogWriterAlias; +use Icinga\Module\Director\Daemon\Logger; +use Icinga\Module\Director\Objects\DirectorJob; +use React\EventLoop\Factory as Loop; +use React\EventLoop\LoopInterface; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; + +class JobsCommand extends Command +{ + public function runAction() + { + $this->app->getModuleManager()->loadEnabledModules(); + $loop = Loop::create(); + if ($this->params->get('rpc')) { + $this->enableRpc($loop); + } + if ($this->params->get('rpc') && $jobId = $this->params->get('id')) { + $exitCode = 1; + $jobId = (int) $jobId; + $loop->futureTick(function () use ($jobId, $loop, &$exitCode) { + Process::setTitle('icinga::director::job'); + try { + $this->raiseLimits(); + $job = DirectorJob::loadWithAutoIncId($jobId, $this->db()); + Process::setTitle('icinga::director::job (' . $job->get('job_name') . ')'); + if ($job->run()) { + $exitCode = 0; + } else { + $exitCode = 1; + } + } catch (Exception $e) { + Logger::error($e->getMessage()); + $exitCode = 1; + } + $loop->futureTick(function () use ($loop) { + $loop->stop(); + }); + }); + } else { + Logger::error('This command is no longer available. Please check our Upgrading documentation'); + $exitCode = 1; + } + + $loop->run(); + exit($exitCode); + } + + protected function enableRpc(LoopInterface $loop) + { + // stream_set_blocking(STDIN, 0); + // stream_set_blocking(STDOUT, 0); + // print_r(stream_get_meta_data(STDIN)); + // stream_set_write_buffer(STDOUT, 0); + // ini_set('implicit_flush', 1); + $netString = new StreamWrapper( + new ReadableResourceStream(STDIN, $loop), + new WritableResourceStream(STDOUT, $loop) + ); + $jsonRpc = new Connection(); + $jsonRpc->handle($netString); + + Logger::replaceRunningInstance(new JsonRpcLogWriterAlias($jsonRpc)); + } +} diff --git a/application/clicommands/KickstartCommand.php b/application/clicommands/KickstartCommand.php new file mode 100644 index 0000000..80aa183 --- /dev/null +++ b/application/clicommands/KickstartCommand.php @@ -0,0 +1,88 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\KickstartHelper; + +/** + * Kickstart a Director installation + * + * Once you prepared your DB resource this command retrieves information about + * unapplied database migration and helps applying them. + */ +class KickstartCommand extends Command +{ + /** + * Check whether a kickstart run is required + * + * This is the case when there is a kickstart.ini in your Directors config + * directory and no ApiUser in your Director DB. + * + * This is mostly for automation, so one could create a Puppet manifest + * as follows: + * + * exec { 'Icinga Director Kickstart': + * path => '/usr/local/bin:/usr/bin:/bin', + * command => 'icingacli director kickstart run', + * onlyif => 'icingacli director kickstart required', + * require => Exec['Icinga Director DB migration'], + * } + * + * Exit code 0 means that a kickstart run is required, code 2 that it is + * not. + */ + public function requiredAction() + { + if ($this->kickstart()->isConfigured()) { + if ($this->kickstart()->isRequired()) { + if ($this->isVerbose) { + echo "Kickstart has been configured and should be triggered\n"; + } + + exit(0); + } else { + echo "Kickstart configured, execution is not required\n"; + exit(1); + } + } else { + echo "Kickstart has not been configured\n"; + exit(2); + } + } + + /** + * Trigger the kickstart helper + * + * This will connect to the endpoint configured in your kickstart.ini, + * store the given API user and import existing objects like zones, + * endpoints and commands. + * + * /etc/icingaweb2/modules/director/kickstart.ini could look as follows: + * + * [config] + * endpoint = "master-node.example.com" + * + * ; Host can be an IP address or a hostname. Equals to endpoint name + * ; if not set: + * host = "127.0.0.1" + * + * ; Port is 5665 if none given + * port = 5665 + * + * username = "director" + * password = "***" + * + */ + public function runAction() + { + $this->raiseLimits(); + $this->kickstart()->loadConfigFromFile()->run(); + exit(0); + } + + protected function kickstart() + { + return new KickstartHelper($this->db()); + } +} diff --git a/application/clicommands/MigrationCommand.php b/application/clicommands/MigrationCommand.php new file mode 100644 index 0000000..6a4d002 --- /dev/null +++ b/application/clicommands/MigrationCommand.php @@ -0,0 +1,66 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Db\Migrations; + +/** + * Handle DB migrations + * + * This command retrieves information about unapplied database migration and + * helps applying them. + */ +class MigrationCommand extends Command +{ + /** + * Check whether there are pending migrations + * + * This is mostly for automation, so one could create a Puppet manifest + * as follows: + * + * exec { 'Icinga Director DB migration': + * command => 'icingacli director migration run', + * onlyif => 'icingacli director migration pending', + * } + * + * Exit code 0 means that there are pending migrations, code 1 that there + * are no such. Use --verbose for human readable output + */ + public function pendingAction() + { + if ($count = $this->migrations()->countPendingMigrations()) { + if ($this->isVerbose) { + if ($count === 1) { + echo "There is 1 pending migration\n"; + } else { + printf("There are %d pending migrations\n", $count); + } + } + + exit(0); + } else { + if ($this->isVerbose) { + echo "There are no pending migrations\n"; + } + + exit(1); + } + } + + /** + * Run any pending migrations + * + * All pending migrations will be silently applied + */ + public function runAction() + { + $this->migrations()->applyPendingMigrations(); + exit(0); + } + + protected function migrations() + { + return new Migrations($this->db()); + } +} diff --git a/application/clicommands/NotificationCommand.php b/application/clicommands/NotificationCommand.php new file mode 100644 index 0000000..bb5402a --- /dev/null +++ b/application/clicommands/NotificationCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Notifications + * + * Use this command to show, create, modify or delete Icinga Notification + * objects + */ +class NotificationCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/ServiceCommand.php b/application/clicommands/ServiceCommand.php new file mode 100644 index 0000000..1bd21e7 --- /dev/null +++ b/application/clicommands/ServiceCommand.php @@ -0,0 +1,92 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Cli\Params; +use Icinga\Module\Director\Cli\ObjectCommand; +use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Resolver\OverrideHelper; +use InvalidArgumentException; + +/** + * Manage Icinga Services + * + * Use this command to show, create, modify or delete Icinga Service + * objects + */ +class ServiceCommand extends ObjectCommand +{ + public function setAction() + { + if (($host = $this->params->get('host')) && $this->params->shift('allow-overrides')) { + if ($this->setServiceProperties($host)) { + return; + } + } + + parent::setAction(); + } + + protected function setServiceProperties($hostname) + { + $serviceName = $this->getName(); + $host = IcingaHost::load($hostname, $this->db()); + $service = ServiceFinder::find($host, $serviceName); + if ($service->requiresOverrides()) { + self::checkForOverrideSafety($this->params); + $properties = $this->remainingParams(); + unset($properties['host']); + OverrideHelper::applyOverriddenVars($host, $serviceName, $properties); + $this->persistChanges($host, 'Host', $hostname . " (Overrides for $serviceName)", 'modified'); + return true; + } + + return false; + } + + protected static function checkForOverrideSafety(Params $params) + { + if ($params->shift('replace')) { + throw new InvalidArgumentException('--replace is not available for Variable Overrides'); + } + $appends = self::stripPrefixedProperties($params, 'append-'); + $remove = self::stripPrefixedProperties($params, 'remove-'); + OverrideHelper::assertVarsForOverrides($appends); + OverrideHelper::assertVarsForOverrides($remove); + if (!empty($appends)) { + throw new InvalidArgumentException('--append- is not available for Variable Overrides'); + } + if (!empty($remove)) { + throw new InvalidArgumentException('--remove- is not available for Variable Overrides'); + } + // Alternative, untested: + // $this->appendToArrayProperties($object, $appends); + // $this->removeProperties($object, $remove); + } + + protected function load($name) + { + return parent::load($this->makeServiceKey($name)); + } + + protected function exists($name) + { + return parent::exists($this->makeServiceKey($name)); + } + + protected function makeServiceKey($name) + { + if ($host = $this->params->get('host')) { + return [ + 'object_name' => $name, + 'host_id' => IcingaHost::load($host, $this->db())->get('id'), + ]; + } else { + return [ + 'object_name' => $name, + 'object_type' => 'template', + ]; + } + } +} diff --git a/application/clicommands/ServicegroupCommand.php b/application/clicommands/ServicegroupCommand.php new file mode 100644 index 0000000..1c732d4 --- /dev/null +++ b/application/clicommands/ServicegroupCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Servicegroups + * + * Use this command to show, create, modify or delete Icinga Servicegroups + * objects + */ +class ServiceGroupCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/ServicesetCommand.php b/application/clicommands/ServicesetCommand.php new file mode 100644 index 0000000..648a42c --- /dev/null +++ b/application/clicommands/ServicesetCommand.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +/** + * Manage Icinga Service Sets + * + * Use this command to show, create, modify or delete Icinga Service + * objects + */ +class ServicesetCommand extends ServiceCommand +{ + protected $type = 'ServiceSet'; +} diff --git a/application/clicommands/ServicesetsCommand.php b/application/clicommands/ServicesetsCommand.php new file mode 100644 index 0000000..54669d5 --- /dev/null +++ b/application/clicommands/ServicesetsCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectsCommand; + +/** + * Manage Icinga Service Sets + * + * Use this command to list Icinga Service Set objects + */ +class ServicesetsCommand extends ObjectsCommand +{ + protected $type = 'ServiceSet'; +} diff --git a/application/clicommands/SyncruleCommand.php b/application/clicommands/SyncruleCommand.php new file mode 100644 index 0000000..37a3f0e --- /dev/null +++ b/application/clicommands/SyncruleCommand.php @@ -0,0 +1,195 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Objects\DirectorActivityLog; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\SyncRule; +use RuntimeException; + +/** + * Deal with Director Sync Rules + * + * Use this command to check or trigger your defined Sync Rules + */ +class SyncruleCommand extends Command +{ + /** + * List defined Sync Rules + * + * This shows a table with your defined Sync Rules, their IDs and + * current state. As triggering a Sync requires an ID, this is where + * you can look up the desired ID. + * + * USAGE + * + * icingacli director syncrule list + */ + public function listAction() + { + $rules = SyncRule::loadAll($this->db()); + if (empty($rules)) { + echo "No Sync Rule has been defined\n"; + + return; + } + + printf("%4s | %s\n", 'ID', 'Sync Rule name'); + printf("-----+%s\n", str_repeat('-', 64)); + + foreach ($rules as $rule) { + $state = $rule->get('sync_state'); + printf("%4d | %s\n", $rule->get('id'), $rule->get('rule_name')); + printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $rule->get('last_error_message') : ''); + } + } + + /** + * Check a given Sync Rule for changes + * + * This command runs a complete Sync in memory but doesn't persist eventual changes. + * + * USAGE + * + * icingacli director syncrule check --id <id> + * + * OPTIONS + * + * --id <id> A Sync Rule ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function checkAction() + { + $rule = $this->getSyncRule(); + $hasChanges = $rule->checkForChanges(); + $this->showSyncStateDetails($rule); + if ($hasChanges) { + $mods = $this->getExpectedModificationCounts($rule); + printf( + "Expected modifications: %dx create, %dx modify, %dx delete\n", + $mods->modify, + $mods->create, + $mods->delete + ); + } + + exit($this->getSyncStateExitCode($rule)); + } + + protected function getExpectedModificationCounts(SyncRule $rule) + { + $modifications = $rule->getExpectedModifications(); + + $create = 0; + $modify = 0; + $delete = 0; + + /** @var IcingaObject $object */ + foreach ($modifications as $object) { + if ($object->hasBeenLoadedFromDb()) { + if ($object->shouldBeRemoved()) { + $delete++; + } else { + $modify++; + } + } else { + $create++; + } + } + + return (object) [ + DirectorActivityLog::ACTION_CREATE => $create, + DirectorActivityLog::ACTION_MODIFY => $modify, + DirectorActivityLog::ACTION_DELETE => $delete, + ]; + } + + /** + * Trigger a Sync Run for a given Sync Rule + * + * This command builds new objects according your Sync Rule, compares them + * with existing ones and persists eventual changes. + * + * USAGE + * + * icingacli director syncrule run --id <id> + * + * OPTIONS + * + * --id <id> A Sync Rule ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function runAction() + { + $rule = $this->getSyncRule(); + + if ($rule->applyChanges()) { + print "New data has been imported\n"; + $this->showSyncStateDetails($rule); + } else { + print "Nothing has been changed, imported data is still up to date\n"; + } + } + + /** + * @return SyncRule + */ + protected function getSyncRule() + { + return SyncRule::loadWithAutoIncId( + (int) $this->params->getRequired('id'), + $this->db() + ); + } + + /** + * @param SyncRule $rule + */ + protected function showSyncStateDetails(SyncRule $rule) + { + echo $this->getSyncStateDescription($rule) . "\n"; + } + + /** + * @param SyncRule $rule + * @return string + */ + protected function getSyncStateDescription(SyncRule $rule) + { + switch ($rule->get('sync_state')) { + case 'unknown': + return "It's currently unknown whether we are in sync with this rule." + . ' You should either check for changes or trigger a new Sync Run.'; + case 'in-sync': + return 'This Sync Rule is in sync'; + case 'pending-changes': + return 'There are pending changes for this Sync Rule. You should' + . ' trigger a new Sync Run.'; + case 'failing': + return 'This Sync Rule failed: '. $rule->get('last_error_message'); + default: + throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state')); + } + } + + /** + * @param SyncRule $rule + * @return string + */ + protected function getSyncStateExitCode(SyncRule $rule) + { + switch ($rule->get('sync_state')) { + case 'unknown': + return 3; + case 'in-sync': + return 0; + case 'pending-changes': + return 1; + case 'failing': + return 2; + default: + throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state')); + } + } +} diff --git a/application/clicommands/TimeperiodCommand.php b/application/clicommands/TimeperiodCommand.php new file mode 100644 index 0000000..352289a --- /dev/null +++ b/application/clicommands/TimeperiodCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Timeperiods + * + * Use this command to show, create, modify or delete Icinga Timeperiod + * objects + */ +class TimePeriodCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/UserCommand.php b/application/clicommands/UserCommand.php new file mode 100644 index 0000000..9c4c9d4 --- /dev/null +++ b/application/clicommands/UserCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Users + * + * Use this command to show, create, modify or delete Icinga User + * objects + */ +class UserCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/UsergroupCommand.php b/application/clicommands/UsergroupCommand.php new file mode 100644 index 0000000..04ba7c3 --- /dev/null +++ b/application/clicommands/UsergroupCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Usergroups + * + * Use this command to show, create, modify or delete Icinga Usergroup + * objects + */ +class UsergroupCommand extends ObjectCommand +{ +} diff --git a/application/clicommands/ZoneCommand.php b/application/clicommands/ZoneCommand.php new file mode 100644 index 0000000..a5c45f9 --- /dev/null +++ b/application/clicommands/ZoneCommand.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Clicommands; + +use Icinga\Module\Director\Cli\ObjectCommand; + +/** + * Manage Icinga Zones + * + * Use this command to show, create, modify or delete Icinga Zone + * objects + */ +class ZoneCommand extends ObjectCommand +{ +} diff --git a/application/controllers/ApiuserController.php b/application/controllers/ApiuserController.php new file mode 100644 index 0000000..36438ae --- /dev/null +++ b/application/controllers/ApiuserController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class ApiuserController extends ObjectController +{ +} diff --git a/application/controllers/ApiusersController.php b/application/controllers/ApiusersController.php new file mode 100644 index 0000000..5597521 --- /dev/null +++ b/application/controllers/ApiusersController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ApiusersController extends ObjectsController +{ +} diff --git a/application/controllers/BasketController.php b/application/controllers/BasketController.php new file mode 100644 index 0000000..8733d16 --- /dev/null +++ b/application/controllers/BasketController.php @@ -0,0 +1,416 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Diff\HtmlRenderer\InlineDiff; +use gipfl\Diff\PhpDiff; +use gipfl\IcingaWeb2\Link; +use gipfl\Web\Table\NameValueTable; +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver; +use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject; +use Icinga\Module\Director\Forms\AddToBasketForm; +use Icinga\Module\Director\Forms\BasketCreateSnapshotForm; +use Icinga\Module\Director\Forms\BasketForm; +use Icinga\Module\Director\Forms\BasketUploadForm; +use Icinga\Module\Director\Forms\RestoreBasketForm; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; +use Icinga\Module\Director\Web\Table\BasketSnapshotTable; + +class BasketController extends ActionController +{ + protected $isApified = true; + + protected function basketTabs() + { + $name = $this->params->get('name'); + return $this->tabs()->add('show', [ + 'label' => $this->translate('Basket'), + 'url' => 'director/basket', + 'urlParams' => ['name' => $name] + ])->add('snapshots', [ + 'label' => $this->translate('Snapshots'), + 'url' => 'director/basket/snapshots', + 'urlParams' => ['name' => $name] + ]); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\MissingParameterException + */ + public function indexAction() + { + $this->actions()->add( + Link::create( + $this->translate('Back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $basket = $this->requireBasket(); + $this->basketTabs()->activate('show'); + $this->addTitle($basket->get('basket_name')); + if ($basket->isEmpty()) { + $this->content()->add(Hint::info($this->translate('This basket is empty'))); + } + $this->content()->add( + (new BasketForm())->setObject($basket)->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + */ + public function addAction() + { + $this->actions()->add( + Link::create( + $this->translate('Baskets'), + 'director/baskets', + null, + ['class' => 'icon-tag'] + ) + ); + $this->addSingleTab($this->translate('Add to Basket')); + $this->addTitle($this->translate('Add chosen objects to a Configuration Basket')); + $form = new AddToBasketForm(); + $form->setDb($this->db()) + ->setType($this->params->getRequired('type')) + ->setNames($this->url()->getParams()->getValues('names')) + ->handleRequest(); + $this->content()->add($form); + } + + public function createAction() + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $this->addSingleTab($this->translate('Create Basket')); + $this->addTitle($this->translate('Create a new Configuration Basket')); + $form = (new BasketForm()) + ->setDb($this->db()) + ->handleRequest(); + $this->content()->add($form); + } + + public function uploadAction() + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $this->addSingleTab($this->translate('Upload a Basket')); + $this->addTitle($this->translate('Upload a Configuration Basket')); + $form = (new BasketUploadForm()) + ->setDb($this->db()) + ->handleRequest(); + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotsAction() + { + $name = $this->params->get('name'); + if ($name === null || $name === '') { + $basket = null; + } else { + $basket = Basket::load($name, $this->db()); + } + if ($basket === null) { + $this->addTitle($this->translate('Basket Snapshots')); + $this->addSingleTab($this->translate('Snapshots')); + } else { + $this->addTitle(sprintf( + $this->translate('%s: Snapshots'), + $basket->get('basket_name') + )); + $this->basketTabs()->activate('snapshots'); + } + if ($basket !== null) { + $this->content()->add( + (new BasketCreateSnapshotForm()) + ->setBasket($basket) + ->handleRequest() + ); + } + $table = new BasketSnapshotTable($this->db()); + if ($basket !== null) { + $table->setBasket($basket); + } + + $table->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::load([ + 'basket_uuid' => $basket->get('uuid'), + 'ts_create' => $this->params->getRequired('ts'), + ], $this->db()); + $snapSum = bin2hex($snapshot->get('content_checksum')); + + if ($this->params->get('action') === 'download') { + $this->getResponse()->setHeader('Content-Type', 'application/json', true); + $this->getResponse()->setHeader('Content-Disposition', sprintf( + 'attachment; filename=Director-Basket_%s_%s.json', + str_replace([' ', '"'], ['_', '_'], iconv( + 'UTF-8', + 'ISO-8859-1//IGNORE', + $basket->get('basket_name') + )), + substr($snapSum, 0, 7) + )); + echo $snapshot->getJsonDump(); + return; + } + + $this->addTitle( + $this->translate('%s: %s (Snapshot)'), + $basket->get('basket_name'), + substr($snapSum, 0, 7) + ); + + $this->actions()->add([ + Link::create( + $this->translate('Show Basket'), + 'director/basket', + ['name' => $basket->get('basket_name')], + ['data-base-target' => '_next'] + ), + Link::create( + $this->translate('Restore'), + $this->url()->with('action', 'restore'), + null, + ['class' => 'icon-rewind'] + ), + Link::create( + $this->translate('Download'), + $this->url() + ->with([ + 'action' => 'download', + 'dbResourceName' => $this->getDbResourceName() + ]), + null, + [ + 'class' => 'icon-download', + 'target' => '_blank' + ] + ), + ]); + + $properties = new NameValueTable(); + $properties->addNameValuePairs([ + $this->translate('Created') => DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000), + $this->translate('Content Checksum') => bin2hex($snapshot->get('content_checksum')), + ]); + $this->content()->add($properties); + + if ($this->params->get('action') === 'restore') { + $form = new RestoreBasketForm(); + $form + ->setSnapshot($snapshot) + ->handleRequest(); + $this->content()->add($form); + $targetDbName = $form->getValue('target_db'); + $connection = $form->getDb(); + } else { + $targetDbName = null; + $connection = $this->db(); + } + + $json = $snapshot->getJsonDump(); + $this->addSingleTab($this->translate('Snapshot')); + $all = Json::decode($json); + $exporter = new Exporter($this->db()); + $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); + foreach ($all as $type => $objects) { + if ($type === 'Datafield') { + // TODO: we should now be able to show all fields and link + // to a "diff" for the ones that should be created + // $this->content()->add(Html::tag('h2', sprintf('+%d Datafield(s)', count($objects)))); + continue; + } + $table = new NameValueTable(); + $table->addAttributes([ + 'class' => ['table-basket-changes', 'table-row-selectable'], + 'data-base-target' => '_next', + ]); + foreach ($objects as $key => $object) { + $linkParams = [ + 'name' => $basket->get('basket_name'), + 'checksum' => $this->params->get('checksum'), + 'ts' => $this->params->get('ts'), + 'type' => $type, + 'key' => $key, + ]; + if ($targetDbName !== null) { + $linkParams['target_db'] = $targetDbName; + } + try { + $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection); + if ($current === null) { + $table->addNameValueRow( + $key, + Link::create( + Html::tag('strong', ['style' => 'color: green'], $this->translate('new')), + 'director/basket/snapshotobject', + $linkParams + ) + ); + continue; + } + $currentExport = $exporter->export($current); + $fieldResolver->tweakTargetIds($currentExport); + + // Ignore originalId + if (isset($currentExport->originalId)) { + unset($currentExport->originalId); + } + if (isset($object->originalId)) { + unset($object->originalId); + } + $hasChanged = ! CompareBasketObject::equals($currentExport, $object); + $table->addNameValueRow( + $key, + $hasChanged + ? Link::create( + Html::tag('strong', ['style' => 'color: orange'], $this->translate('modified')), + 'director/basket/snapshotobject', + $linkParams + ) + : Html::tag('span', ['style' => 'color: green'], $this->translate('unchanged')) + ); + } catch (Exception $e) { + $table->addNameValueRow( + $key, + Html::tag('a', sprintf( + '%s (%s:%d)', + $e->getMessage(), + basename($e->getFile()), + $e->getLine() + )) + ); + } + } + $this->content()->add(Html::tag('h2', $type)); + $this->content()->add($table); + } + $this->content()->add(Html::tag('div', ['style' => 'height: 5em'])); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotobjectAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::load([ + 'basket_uuid' => $basket->get('uuid'), + 'ts_create' => $this->params->getRequired('ts'), + ], $this->db()); + $snapshotUrl = $this->url()->without('type')->without('key')->setPath('director/basket/snapshot'); + $type = $this->params->get('type'); + $key = $this->params->get('key'); + + $this->addTitle($this->translate('Single Object Diff')); + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('Comparing %s "%s" from Snapshot "%s" to current config'), + $type, + $key, + Link::create( + substr(bin2hex($snapshot->get('content_checksum')), 0, 7), + $snapshotUrl, + null, + ['data-base-target' => '_next'] + ) + ))); + $this->actions()->add([ + Link::create( + $this->translate('back'), + $snapshotUrl, + null, + ['class' => 'icon-left-big'] + ), + /* + Link::create( + $this->translate('Restore'), + $this->url()->with('action', 'restore'), + null, + ['class' => 'icon-rewind'] + ) + */ + ]); + $exporter = new Exporter($this->db()); + $json = $snapshot->getJsonDump(); + $this->addSingleTab($this->translate('Snapshot')); + $objects = Json::decode($json); + $targetDbName = $this->params->get('target_db'); + if ($targetDbName === null) { + $connection = $this->db(); + } else { + $connection = Db::fromResourceName($targetDbName); + } + $fieldResolver = new BasketSnapshotFieldResolver($objects, $connection); + $objectFromBasket = $objects->$type->$key; + unset($objectFromBasket->originalId); + CompareBasketObject::normalize($objectFromBasket); + $objectFromBasket = Json::encode($objectFromBasket, JSON_PRETTY_PRINT); + $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection); + if ($current === null) { + $current = ''; + } else { + $exported = $exporter->export($current); + $fieldResolver->tweakTargetIds($exported); + unset($exported->originalId); + CompareBasketObject::normalize($exported); + $current = Json::encode($exported, JSON_PRETTY_PRINT); + } + + if ($current === $objectFromBasket) { + $this->content()->add([ + Hint::ok('Basket equals current object'), + Html::tag('pre', $current) + ]); + } else { + $this->content()->add(new InlineDiff(new PhpDiff($current, $objectFromBasket))); + } + } + + /** + * @return Basket + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireBasket() + { + return Basket::load($this->params->getRequired('name'), $this->db()); + } +} diff --git a/application/controllers/BasketsController.php b/application/controllers/BasketsController.php new file mode 100644 index 0000000..6b50b62 --- /dev/null +++ b/application/controllers/BasketsController.php @@ -0,0 +1,53 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\BasketTable; + +class BasketsController extends ActionController +{ + protected $isApified = false; + + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->addSingleTab($this->translate('Baskets')); + $this->actions()->add([ + Link::create( + $this->translate('Create'), + 'director/basket/create', + null, + ['class' => 'icon-plus'] + ), + Link::create( + $this->translate('Upload'), + 'director/basket/upload', + null, + ['class' => 'icon-upload'] + ), + ]); + $this->addTitle($this->translate('Configuration Baskets')); + $this->content()->add(Html::tag('p', $this->translate( + 'A Configuration Basket references specific Configuration' + . ' Objects or all objects of a specific type. It has been' + . ' designed to share Templates, Import/Sync strategies and' + . ' other base Configuration Objects. It is not a tool to' + . ' operate with single Hosts or Services.' + ))); + $this->content()->add(Html::tag('p', $this->translate( + 'You can create Basket snapshots at any time, this will persist' + . ' a serialized representation of all involved objects at that' + . ' moment in time. Snapshots can be exported, imported, shared' + . ' and restored - to the very same or another Director instance.' + ))); + $table = (new BasketTable($this->db())) + ->setAttribute('data-base-target', '_self'); + // TODO: temporarily disabled, this was a thing in dipl + if (/*$table->hasSearch() || */count($table)) { + $table->renderTo($this); + } + } +} diff --git a/application/controllers/BranchController.php b/application/controllers/BranchController.php new file mode 100644 index 0000000..3b36e83 --- /dev/null +++ b/application/controllers/BranchController.php @@ -0,0 +1,138 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Diff\HtmlRenderer\SideBySideDiff; +use gipfl\Diff\PhpDiff; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\BranchActivity; +use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\SyncRule; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Widget\IcingaConfigDiff; +use ipl\Html\Html; + +class BranchController extends ActionController +{ + use BranchHelper; + + public function init() + { + parent::init(); + IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); + SyncRule::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); + } + + protected function checkDirectorPermissions() + { + } + + public function activityAction() + { + $this->assertPermission('director/showconfig'); + $ts = $this->params->getRequired('ts'); + $activity = BranchActivity::load($ts, $this->db()); + $store = new BranchStore($this->db()); + $branch = $store->fetchBranchByUuid($activity->getBranchUuid()); + if ($branch->isSyncPreview()) { + $this->addSingleTab($this->translate('Sync Preview')); + $this->addTitle($this->translate('Expected Modification')); + } else { + $this->addSingleTab($this->translate('Activity')); + $this->addTitle($this->translate('Branch Activity')); + } + + $this->content()->add($this->prepareActivityInfo($activity)); + $this->showActivity($activity); + } + + protected function prepareActivityInfo(BranchActivity $activity) + { + $table = new NameValueTable(); + $table->addNameValuePairs([ + $this->translate('Author') => $activity->getAuthor(), + $this->translate('Date') => date('Y-m-d H:i:s', $activity->getTimestamp()), + $this->translate('Action') => $activity->getAction() + . ' ' . preg_replace('/^icinga_/', '', $activity->getObjectTable()) + . ' ' . $activity->getObjectName(), + // $this->translate('Actions') => ['Undo form'], + ]); + return $table; + } + + protected function leftFromActivity(BranchActivity $activity) + { + if ($activity->isActionCreate()) { + return null; + } + $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db()); + $properties = $this->objectTypeFirst($activity->getFormerProperties()->jsonSerialize()); + foreach ($properties as $key => $value) { + $object->set($key, $value); + } + + return $object; + } + + protected function rightFromActivity(BranchActivity $activity) + { + if ($activity->isActionDelete()) { + return null; + } + $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db()); + if (! $activity->isActionCreate()) { + foreach ($activity->getFormerProperties()->jsonSerialize() as $key => $value) { + $object->set($key, $value); + } + } + $properties = $this->objectTypeFirst($activity->getModifiedProperties()->jsonSerialize()); + foreach ($properties as $key => $value) { + $object->set($key, $value); + } + + return $object; + } + + protected function objectTypeFirst($properties) + { + $properties = (array) $properties; + if (isset($properties['object_type'])) { + $type = $properties['object_type']; + unset($properties['object_type']); + $properties = ['object_type' => $type] + $properties; + } + + return $properties; + } + + protected function showActivity(BranchActivity $activity) + { + $left = $this->leftFromActivity($activity); + $right = $this->rightFromActivity($activity); + if ($left instanceof IcingaObject || $right instanceof IcingaObject) { + $this->content()->add(new IcingaConfigDiff( + $left ? $left->toSingleIcingaConfig() : $this->createEmptyConfig(), + $right ? $right->toSingleIcingaConfig() : $this->createEmptyConfig() + )); + } else { + $this->content()->add([ + Html::tag('h3', $this->translate('Modification')), + new SideBySideDiff(new PhpDiff( + PlainObjectRenderer::render($left->getProperties()), + PlainObjectRenderer::render($right->getProperties()) + )) + ]); + } + } + + protected function createEmptyConfig() + { + return new IcingaConfig($this->db()); + } +} diff --git a/application/controllers/CommandController.php b/application/controllers/CommandController.php new file mode 100644 index 0000000..de0ba54 --- /dev/null +++ b/application/controllers/CommandController.php @@ -0,0 +1,126 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Objects\IcingaCommandArgument; +use Icinga\Module\Director\Web\Table\BranchedIcingaCommandArgumentTable; +use ipl\Html\Html; +use Icinga\Module\Director\Forms\IcingaCommandArgumentForm; +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Resolver\CommandUsage; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Table\IcingaCommandArgumentTable; + +class CommandController extends ObjectController +{ + /** + * @throws \Icinga\Exception\AuthenticationException + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function init() + { + parent::init(); + $o = $this->object; + if ($o && ! $o->isExternal()) { + if ($this->getBranch()->isBranch()) { + $urlParams = ['uuid' => $o->getUniqueId()->toString()]; + } else { + $urlParams = ['name' => $o->getObjectName()]; + } + $this->tabs()->add('arguments', [ + 'url' => 'director/command/arguments', + 'urlParams' => $urlParams, + 'label' => 'Arguments' + ]); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Zend_Db_Select_Exception + */ + public function indexAction() + { + if (! $this->getRequest()->isApiRequest()) { + $this->showUsage(); + } + parent::indexAction(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + * @throws \Zend_Db_Select_Exception + */ + public function renderAction() + { + if ($this->object->isExternal()) { + $this->showUsage(); + } + + parent::renderAction(); + } + + /** + * @throws \Zend_Db_Select_Exception + */ + protected function showUsage() + { + /** @var IcingaCommand $command */ + $command = $this->object; + if ($command->isInUse()) { + $usage = new CommandUsage($command); + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('This Command is currently being used by %s'), + Html::tag('span', null, $usage->getLinks())->setSeparator(', ') + ))->addAttributes([ + 'data-base-target' => '_next' + ])); + } else { + $this->content()->add(Hint::warning($this->translate('This Command is currently not in use'))); + } + } + + public function argumentsAction() + { + $p = $this->params; + /** @var IcingaCommand $o */ + $o = $this->object; + $this->tabs()->activate('arguments'); + $this->addTitle($this->translate('Command arguments: %s'), $o->getObjectName()); + $form = (new IcingaCommandArgumentForm) + ->setBranch($this->getBranch()) + ->setCommandObject($o); + if ($argument = $p->shift('argument')) { + $this->addBackLink('director/command/arguments', [ + 'name' => $p->get('name') + ]); + if ($this->branch->isBranch()) { + $arguments = $o->arguments(); + $argument = $arguments->get($argument); + // IcingaCommandArgument::create((array) $arguments->get($argument)->toFullPlainObject()); + // $argument->setBeingLoadedFromDb(); + } else { + $argument = IcingaCommandArgument::load([ + 'command_id' => $o->get('id'), + 'argument_name' => $argument + ], $this->db()); + } + $form->setObject($argument); + } + $form->handleRequest(); + $this->content()->add([$form]); + if ($this->branch->isBranch()) { + (new BranchedIcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this); + } else { + (new IcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this); + } + } + + protected function hasBasketSupport() + { + return true; + } +} diff --git a/application/controllers/CommandsController.php b/application/controllers/CommandsController.php new file mode 100644 index 0000000..246028f --- /dev/null +++ b/application/controllers/CommandsController.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class CommandsController extends ObjectsController +{ + public function indexAction() + { + parent::indexAction(); + $validTypes = ['object', 'external_object']; + $type = $this->params->get('type', 'object'); + if (! in_array($type, $validTypes)) { + $type = 'object'; + } + + $this->table->setType($type); + } +} diff --git a/application/controllers/CommandtemplateController.php b/application/controllers/CommandtemplateController.php new file mode 100644 index 0000000..ca5f827 --- /dev/null +++ b/application/controllers/CommandtemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class CommandtemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaCommand::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 0000000..3f8a105 --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,539 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Diff\HtmlRenderer\SideBySideDiff; +use gipfl\Diff\PhpDiff; +use gipfl\Web\Widget\Hint; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Deployment\DeploymentStatus; +use Icinga\Module\Director\Forms\DeployConfigForm; +use Icinga\Module\Director\Forms\SettingsForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Table\ActivityLogTable; +use Icinga\Module\Director\Web\Table\BranchActivityTable; +use Icinga\Module\Director\Web\Table\ConfigFileDiffTable; +use Icinga\Module\Director\Web\Table\DeploymentLogTable; +use Icinga\Module\Director\Web\Table\GeneratedConfigFileTable; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Tabs\InfraTabs; +use Icinga\Module\Director\Web\Widget\ActivityLogInfo; +use Icinga\Module\Director\Web\Widget\DeployedConfigInfoHeader; +use Icinga\Module\Director\Web\Widget\ShowConfigFile; +use Icinga\Web\Notification; +use Exception; +use RuntimeException; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; + +class ConfigController extends ActionController +{ + use BranchHelper; + + protected $isApified = true; + + protected function checkDirectorPermissions() + { + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function deploymentsAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/deploy'); + $this->addTitle($this->translate('Deployments')); + try { + if (DirectorDeploymentLog::hasUncollected($this->db())) { + $this->setAutorefreshInterval(2); + } else { + $this->setAutorefreshInterval(20); + } + } catch (Exception $e) { + $this->content()->prepend(Hint::warning($e->getMessage())); + // No problem, Icinga might be reloading + } + + if (! $this->getBranch()->isBranch()) { + // TODO: a form! + $this->actions()->add(Link::create( + $this->translate('Render config'), + 'director/config/store', + null, + ['class' => 'icon-wrench'] + )); + } + + $this->tabs(new InfraTabs($this->Auth()))->activate('deploymentlog'); + $table = new DeploymentLogTable($this->db()); + try { + // Move elsewhere + $table->setActiveStageName( + $this->api()->getActiveStageName() + ); + } catch (Exception $e) { + // Don't care + } + + $table->renderTo($this); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + * @throws \Icinga\Security\SecurityException + */ + public function deployAction() + { + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + if (! $request->isPost()) { + throw new RuntimeException(sprintf( + 'Unsupported method: %s', + $request->getMethod() + )); + } + $this->assertPermission('director/deploy'); + + // TODO: require POST + $checksum = $this->params->get('checksum'); + if ($checksum) { + $config = IcingaConfig::load(hex2bin($checksum), $this->db()); + } else { + $config = IcingaConfig::generate($this->db()); + $checksum = $config->getHexChecksum(); + } + + try { + $this->api()->wipeInactiveStages($this->db()); + } catch (Exception $e) { + $this->deploymentFailed($checksum, $e->getMessage()); + } + + if ($this->api()->dumpConfig($config, $this->db())) { + $this->deploymentSucceeded($checksum); + } else { + $this->deploymentFailed($checksum); + } + } + + public function deploymentStatusAction() + { + if ($this->sendNotFoundUnlessRestApi()) { + return; + } + $db = $this->db(); + $api = $this->api(); + $status = new DeploymentStatus($db, $api); + $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities')); + + $this->sendJson($this->getResponse(), (object) $result); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function activitiesAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/audit'); + $this->showOptionalBranchActivity(); + $this->setAutorefreshInterval(10); + $this->tabs(new InfraTabs($this->Auth()))->activate('activitylog'); + $this->addTitle($this->translate('Activity Log')); + $lastDeployedId = $this->db()->getLastDeploymentActivityLogId(); + $table = new ActivityLogTable($this->db()); + $table->setLastDeployedId($lastDeployedId); + if ($idRangeEx = $this->url()->getParam('idRangeEx')) { + $table->applyFilter(Filter::fromQueryString($idRangeEx)); + } + $filter = Filter::fromQueryString( + $this->url()->without(['page', 'limit', 'q', 'idRangeEx'])->getQueryString() + ); + $table->applyFilter($filter); + if ($this->url()->hasParam('author')) { + $this->actions()->add(Link::create( + $this->translate('All changes'), + $this->url() + ->without(['author', 'page']), + null, + ['class' => 'icon-users', 'data-base-target' => '_self'] + )); + } else { + $this->actions()->add(Link::create( + $this->translate('My changes'), + $this->url() + ->with('author', $this->Auth()->getUser()->getUsername()) + ->without('page'), + null, + ['class' => 'icon-user', 'data-base-target' => '_self'] + )); + } + if ($this->hasPermission('director/deploy') && ! $this->getBranch()->isBranch()) { + if ($this->db()->hasDeploymentEndpoint()) { + $this->actions()->add(DeployConfigForm::load() + ->setDb($this->db()) + ->setApi($this->api()) + ->handleRequest()); + } + } + + $table->renderTo($this); + } + + /** + * @throws IcingaException + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\ProgrammingError + */ + public function activityAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $p = $this->params; + $info = new ActivityLogInfo( + $this->db(), + $p->get('type'), + $p->get('name') + ); + + $info->setChecksum($p->get('checksum')) + ->setId($p->get('id')); + + $this->tabs($info->getTabs($this->url())); + $info->showTab($this->params->get('show')); + + $this->addTitle($info->getTitle()); + $this->controls()->prepend($info->getPagination($this->url())); + $this->content()->add($info); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function settingsAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/admin'); + + $this->addSingleTab($this->translate('Settings')) + ->addTitle($this->translate('Global Director Settings')); + $this->content()->add( + SettingsForm::load() + ->setSettings(new Settings($this->db())) + ->handleRequest() + ); + } + + /** + * Show all files for a given config + * + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Security\SecurityException + */ + public function filesAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $config = IcingaConfig::load( + hex2bin($this->params->getRequired('checksum')), + $this->db() + ); + $deploymentId = $this->params->get('deployment_id'); + + $tabs = $this->tabs(); + if ($deploymentId) { + $tabs->add('deployment', [ + 'label' => $this->translate('Deployment'), + 'url' => 'director/deployment', + 'urlParams' => ['id' => $deploymentId] + ]); + } + + $tabs->add('config', [ + 'label' => $this->translate('Config'), + 'url' => $this->url(), + ])->activate('config'); + + $this->addTitle($this->translate('Generated config')); + $this->content()->add(new DeployedConfigInfoHeader( + $config, + $this->db(), + $this->api(), + $this->getBranch(), + $deploymentId + )); + + GeneratedConfigFileTable::load($config, $this->db()) + ->setActiveFilename($this->params->get('active_file')) + ->setDeploymentId($deploymentId) + ->renderTo($this); + } + + /** + * Show a single file + * + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Security\SecurityException + */ + public function fileAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $filename = $this->params->getRequired('file_path'); + $this->configTabs()->add('file', array( + 'label' => $this->translate('Rendered file'), + 'url' => $this->url(), + ))->activate('file'); + + $params = $this->getConfigTabParams(); + if ('deployment' === $this->params->get('backTo')) { + $this->addBackLink('director/deployment', ['id' => $params['deployment_id']]); + } else { + $params['active_file'] = $filename; + $this->addBackLink('director/config/files', $params); + } + + $config = IcingaConfig::load(hex2bin($this->params->get('config_checksum')), $this->db()); + $this->addTitle($this->translate('Config file "%s"'), $filename); + $this->content()->add(new ShowConfigFile( + $config->getFile($filename), + $this->params->get('highlight'), + $this->params->get('highlightSeverity') + )); + } + + /** + * TODO: Check if this can be removed + * + * @throws \Icinga\Security\SecurityException + */ + public function storeAction() + { + $this->assertPermission('director/deploy'); + try { + $config = IcingaConfig::generate($this->db()); + } catch (Exception $e) { + Notification::error($e->getMessage()); + $this->redirectNow('director/config/deployments'); + } + $this->redirectNow( + Url::fromPath( + 'director/config/files', + array('checksum' => $config->getHexChecksum()) + ) + ); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function diffAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + + $db = $this->db(); + $this->addTitle($this->translate('Config diff')); + $this->addSingleTab($this->translate('Config diff')); + + $leftSum = $this->params->get('left'); + $rightSum = $this->params->get('right'); + + $configs = $db->enumDeployedConfigs(); + foreach (array($leftSum, $rightSum) as $sum) { + if (! array_key_exists($sum, $configs)) { + $configs[$sum] = substr($sum, 0, 7); + } + } + + $baseUrl = $this->url()->without(['left', 'right']); + $this->content()->add(Html::tag('form', ['action' => (string) $baseUrl, 'method' => 'GET'], [ + new HtmlString($this->view->formSelect( + 'left', + $leftSum, + ['class' => 'autosubmit', 'style' => 'width: 37%'], + [null => $this->translate('- please choose -')] + $configs + )), + Link::create( + Icon::create('flapping'), + $baseUrl, + ['left' => $rightSum, 'right' => $leftSum] + ), + new HtmlString($this->view->formSelect( + 'right', + $rightSum, + ['class' => 'autosubmit', 'style' => 'width: 37%'], + [null => $this->translate('- please choose -')] + $configs + )), + ])); + + if ($rightSum === null || $leftSum === null || ! strlen($rightSum) || ! strlen($leftSum)) { + return; + } + ConfigFileDiffTable::load($leftSum, $rightSum, $this->db())->renderTo($this); + } + + /** + * @throws IcingaException + * @throws \Icinga\Exception\MissingParameterException + */ + public function filediffAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + + $p = $this->params; + $db = $this->db(); + $leftSum = $p->getRequired('left'); + $rightSum = $p->getRequired('right'); + $filename = $p->getRequired('file_path'); + + $left = IcingaConfig::load(hex2bin($leftSum), $db); + $right = IcingaConfig::load(hex2bin($rightSum), $db); + + $this + ->addTitle($this->translate('Config file "%s"'), $filename) + ->addSingleTab($this->translate('Diff')) + ->content()->add(new SideBySideDiff(new PhpDiff( + $left->getFile($filename), + $right->getFile($filename) + ))); + } + + protected function showOptionalBranchActivity() + { + if ($this->url()->hasParam('idRangeEx')) { + return; + } + $branch = $this->getBranch(); + if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) { + $table = new BranchActivityTable($branch->getUuid(), $this->db()); + if (count($table) > 0) { + $this->content()->add(Hint::info(Html::sprintf($this->translate( + 'The following modifications are visible in this %s only...' + ), Branch::requireHook()->linkToBranch( + $branch, + $this->Auth(), + $this->translate('configuration branch') + )))); + $this->content()->add($table); + $this->content()->add(Html::tag('br')); + $this->content()->add(Hint::ok($this->translate( + '...and the modifications below are already in the main branch:' + ))); + $this->content()->add(Html::tag('br')); + } + } + } + + /** + * @param $checksum + */ + protected function deploymentSucceeded($checksum) + { + if ($this->getRequest()->isApiRequest()) { + $this->sendJson($this->getResponse(), (object) array('checksum' => $checksum)); + return; + } else { + $url = Url::fromPath('director/config/deployments'); + Notification::success( + $this->translate('Config has been submitted, validation is going on') + ); + $this->redirectNow($url); + } + } + + /** + * @param $checksum + * @param null $error + */ + protected function deploymentFailed($checksum, $error = null) + { + $extra = $error ? ': ' . $error: ''; + + if ($this->getRequest()->isApiRequest()) { + $this->sendJsonError($this->getResponse(), 'Config deployment failed' . $extra); + return; + } else { + $url = Url::fromPath('director/config/files', array('checksum' => $checksum)); + Notification::error( + $this->translate('Config deployment failed') . $extra + ); + $this->redirectNow($url); + } + } + + /** + * @return \gipfl\IcingaWeb2\Widget\Tabs + */ + protected function configTabs() + { + $tabs = $this->tabs(); + + if ($this->hasPermission('director/deploy') + && $deploymentId = $this->params->get('deployment_id') + ) { + $tabs->add('deployment', [ + 'label' => $this->translate('Deployment'), + 'url' => 'director/deployment', + 'urlParams' => ['id' => $deploymentId] + ]); + } + + if ($this->hasPermission('director/showconfig')) { + $tabs->add('config', [ + 'label' => $this->translate('Config'), + 'url' => 'director/config/files', + 'urlParams' => $this->getConfigTabParams() + ]); + } + + return $tabs; + } + + protected function getConfigTabParams() + { + $params = [ + 'checksum' => $this->params->get( + 'config_checksum', + $this->params->get('checksum') + ) + ]; + + if ($deploymentId = $this->params->get('deployment_id')) { + $params['deployment_id'] = $deploymentId; + } + + return $params; + } +} diff --git a/application/controllers/CustomvarController.php b/application/controllers/CustomvarController.php new file mode 100644 index 0000000..f0d4574 --- /dev/null +++ b/application/controllers/CustomvarController.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\CustomvarVariantsTable; + +class CustomvarController extends ActionController +{ + public function variantsAction() + { + $varName = $this->params->getRequired('name'); + $this->addSingleTab($this->translate('Custom Variable')) + ->addTitle($this->translate('Custom Variable variants: %s'), $varName); + CustomvarVariantsTable::create($this->db(), $varName)->renderTo($this); + } +} diff --git a/application/controllers/DaemonController.php b/application/controllers/DaemonController.php new file mode 100644 index 0000000..ab0038f --- /dev/null +++ b/application/controllers/DaemonController.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Application\Icinga; +use Icinga\Module\Director\Daemon\RunningDaemonInfo; +use Icinga\Module\Director\Web\Tabs\MainTabs; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Widget\BackgroundDaemonDetails; +use Icinga\Module\Director\Web\Widget\Documentation; +use ipl\Html\Html; + +class DaemonController extends ActionController +{ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('daemon'); + $this->setTitle($this->translate('Director Background Daemon')); + // Avoiding layout issues: + $this->content()->add(Html::tag('h1', $this->translate('Director Background Daemon'))); + // TODO: move dashboard titles into controls. Or figure out whether 2.7 "broke" this + + $error = null; + try { + $db = $this->db()->getDbAdapter(); + $daemons = $db->fetchAll( + $db->select()->from('director_daemon_info')->order('fqdn')->order('username')->order('pid') + ); + } catch (\Exception $e) { + $daemons = []; + $error = $e->getMessage(); + } + + if (empty($daemons)) { + $documentation = new Documentation(Icinga::app(), $this->Auth()); + $message = Html::sprintf($this->translate( + 'The Icinga Director Background Daemon is not running.' + . ' Please check our %s in case you need step by step instructions' + . ' showing you how to fix this.' + ), $documentation->getModuleLink( + $this->translate('documentation'), + 'director', + '75-Background-Daemon', + $this->translate('Icinga Director Background Daemon') + )); + $this->content()->add(Hint::error([ + $message, + ($error ? [Html::tag('br'), Html::tag('strong', $error)] : null), + ])); + return; + } + + try { + foreach ($daemons as $daemon) { + $info = new RunningDaemonInfo($daemon); + $this->content()->add([new BackgroundDaemonDetails($info, $daemon) /*, $logWindow*/]); + } + } catch (\Exception $e) { + $this->content()->add(Hint::error($e->getMessage())); + } + } +} diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php new file mode 100644 index 0000000..95c1cd0 --- /dev/null +++ b/application/controllers/DashboardController.php @@ -0,0 +1,78 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Tabs\MainTabs; +use Icinga\Module\Director\Dashboard\Dashboard; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Form\DbSelectorForm; + +class DashboardController extends ActionController +{ + protected function checkDirectorPermissions() + { + // No special permissions required, override parent method + } + + protected function addDbSelection() + { + if ($this->isMultiDbSetup()) { + $form = new DbSelectorForm( + $this->getResponse(), + $this->Window(), + $this->listAllowedDbResourceNames() + ); + $this->content()->add($form); + $form->handleRequest($this->getServerRequest()); + } + } + + public function indexAction() + { + if ($this->getRequest()->isGet()) { + $this->setAutorefreshInterval(10); + } + + $mainDashboards = [ + 'Objects', + 'Alerts', + 'Branches', + 'Automation', + 'Deployment', + 'Director', + 'Data', + ]; + $this->setTitle($this->translate('Icinga Director - Main Dashboard')); + $names = $this->params->getValues('name', $mainDashboards); + if (! $this->params->has('name')) { + $this->addDbSelection(); + } + if (count($names) === 1) { + $name = $names[0]; + $dashboard = Dashboard::loadByName($name, $this->db()); + $this->tabs($dashboard->getTabs())->activate($name); + } else { + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('main'); + } + + $cntDashboards = 0; + foreach ($names as $name) { + if ($name instanceof Dashboard) { + $dashboard = $name; + } else { + $dashboard = Dashboard::loadByName($name, $this->db()); + } + if ($dashboard->isAvailable()) { + $cntDashboards++; + $this->content()->add($dashboard); + } + } + + if ($cntDashboards === 0) { + $msg = $this->translate( + 'No dashboard available, you might have not enough permissions' + ); + $this->content()->add($msg); + } + } +} diff --git a/application/controllers/DataController.php b/application/controllers/DataController.php new file mode 100644 index 0000000..ae4bbcf --- /dev/null +++ b/application/controllers/DataController.php @@ -0,0 +1,406 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Forms\DirectorDatalistEntryForm; +use Icinga\Module\Director\Forms\DirectorDatalistForm; +use Icinga\Module\Director\Forms\IcingaServiceDictionaryMemberForm; +use Icinga\Module\Director\Objects\DirectorDatalist; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; +use Icinga\Module\Director\Web\Table\CustomvarTable; +use Icinga\Module\Director\Web\Table\DatafieldCategoryTable; +use Icinga\Module\Director\Web\Table\DatafieldTable; +use Icinga\Module\Director\Web\Table\DatalistEntryTable; +use Icinga\Module\Director\Web\Table\DatalistTable; +use Icinga\Module\Director\Web\Tabs\DataTabs; +use gipfl\IcingaWeb2\Link; +use InvalidArgumentException; +use ipl\Html\Html; +use ipl\Html\Table; + +class DataController extends ActionController +{ + public function listsAction() + { + $this->addTitle($this->translate('Data lists')); + $this->actions()->add( + Link::create($this->translate('Add'), 'director/data/list', null, [ + 'class' => 'icon-plus', + 'data-base-target' => '_next' + ]) + ); + + $this->tabs(new DataTabs())->activate('datalist'); + (new DatalistTable($this->db()))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function listAction() + { + $form = DirectorDatalistForm::load() + ->setSuccessUrl('director/data/lists') + ->setDb($this->db()); + + if ($name = $this->params->get('name')) { + $list = $this->requireList('name'); + $form->setObject($list); + $this->addListActions($list); + $this->addTitle( + $this->translate('Data List: %s'), + $list->get('list_name') + )->addListTabs($name, 'list'); + } else { + $this + ->addTitle($this->translate('Add a new Data List')) + ->addSingleTab($this->translate('Data List')); + } + + $this->content()->add($form->handleRequest()); + } + + public function fieldsAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new DataTabs())->activate('datafield'); + $this->addTitle($this->translate('Data Fields')); + $this->actions()->add(Link::create( + $this->translate('Add'), + 'director/datafield/add', + null, + [ + 'class' => 'icon-plus', + 'data-base-target' => '_next', + ] + )); + + (new DatafieldTable($this->db()))->renderTo($this); + } + + public function fieldcategoriesAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new DataTabs())->activate('datafieldcategory'); + $this->addTitle($this->translate('Data Field Categories')); + $this->actions()->add(Link::create( + $this->translate('Add'), + 'director/datafieldcategory/add', + null, + [ + 'class' => 'icon-plus', + 'data-base-target' => '_next', + ] + )); + + (new DatafieldCategoryTable($this->db()))->renderTo($this); + } + + public function varsAction() + { + $this->tabs(new DataTabs())->activate('customvars'); + $this->addTitle($this->translate('Custom Vars - Overview')); + (new CustomvarTable($this->db()))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function listentryAction() + { + $entryName = $this->params->get('entry_name'); + $list = $this->requireList('list'); + $this->addListActions($list); + $listId = $list->get('id'); + $listName = $list->get('list_name'); + $title = $title = $this->translate('List Entries') . ': ' . $listName; + $this->addTitle($title); + + $form = DirectorDatalistEntryForm::load() + ->setSuccessUrl('director/data/listentry', ['list' => $listName]) + ->setList($list); + + if (null !== $entryName) { + $form->loadObject([ + 'list_id' => $listId, + 'entry_name' => $entryName + ]); + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/data/listentry', + ['list' => $listName], + ['class' => 'icon-left-big'] + )); + } + $form->handleRequest(); + + $this->addListTabs($listName, 'entries'); + + $table = new DatalistEntryTable($this->db()); + $table->getAttributes()->set('data-base-target', '_self'); + $table->setList($list); + $this->content()->add([$form, $table]); + } + + public function dictionaryAction() + { + $connection = $this->db(); + $this->addSingleTab('Nested Dictionary'); + $varName = $this->params->get('varname'); + $instance = $this->url()->getParam('instance'); + $action = $this->url()->getParam('action'); + $object = $this->requireObject(); + + if ($instance || $action) { + $this->actions()->add( + Link::create($this->translate('Back'), $this->url()->without(['action', 'instance']), null, [ + 'class' => 'icon-edit' + ]) + ); + } else { + $this->actions()->add( + Link::create($this->translate('Add'), $this->url(), [ + 'action' => 'add' + ], [ + 'class' => 'icon-edit' + ]) + ); + } + $subjects = $this->prepareSubjectsLabel($object, $varName); + $fieldLoader = new IcingaObjectFieldLoader($object); + $instances = $this->getCurrentInstances($object, $varName); + + if (empty($instances)) { + $this->content()->add(Hint::info(sprintf( + $this->translate('No %s have been created yet'), + $subjects + ))); + } else { + $this->content()->add($this->prepareInstancesTable($instances)); + } + + $field = $this->getFieldByName($fieldLoader, $varName); + $template = $object::load([ + 'object_name' => $field->getSetting('template_name') + ], $connection); + + $form = new IcingaServiceDictionaryMemberForm(); + $form->setDb($connection); + if ($instance) { + $instanceObject = $object::create([ + 'imports' => [$template], + 'object_name' => $instance, + 'vars' => $instances[$instance] + ], $connection); + $form->setObject($instanceObject); + } elseif ($action === 'add') { + $form->presetImports([$template->getObjectName()]); + } else { + return; + } + if ($instance) { + if (! isset($instances[$instance])) { + throw new NotFoundError("There is no such instance: $instance"); + } + $subTitle = sprintf($this->translate('Modify instance: %s'), $instance); + } else { + $subTitle = $this->translate('Add a new instance'); + } + + $this->content()->add(Html::tag('h2', ['style' => 'margin-top: 2em'], $subTitle)); + $form->handleRequest($this->getRequest()); + $this->content()->add($form); + if ($form->succeeded()) { + $virtualObject = $form->getObject(); + $name = $virtualObject->getObjectName(); + $params = $form->getObject()->getVars(); + $instances[$name] = $params; + if ($name !== $instance) { // Has been renamed + unset($instances[$instance]); + } + ksort($instances); + $object->set("vars.$varName", (object)$instances); + $object->store(); + $this->redirectNow($this->url()->without(['instance', 'action'])); + } elseif ($form->shouldBeDeleted()) { + unset($instances[$instance]); + if (empty($instances)) { + $object->set("vars.$varName", null)->store(); + } else { + $object->set("vars.$varName", (object)$instances)->store(); + } + $this->redirectNow($this->url()->without(['instance', 'action'])); + } + } + + protected function requireObject() + { + $connection = $this->db(); + $hostName = $this->params->getRequired('host'); + $serviceName = $this->params->get('service'); + if ($serviceName) { + $host = IcingaHost::load($hostName, $connection); + $object = IcingaService::load([ + 'host_id' => $host->get('id'), + 'object_name' => $serviceName, + ], $connection); + } else { + $object = IcingaHost::load($hostName, $connection); + } + + if (! $object->isObject()) { + throw new InvalidArgumentException(sprintf( + 'Only single objects allowed, %s is a %s', + $object->getObjectName(), + $object->get('object_type') + )); + } + return $object; + } + + protected function shorten($string, $maxLen) + { + if (strlen($string) <= $maxLen) { + return $string; + } + + return substr($string, 0, $maxLen) . '...'; + } + + protected function getFieldByName(IcingaObjectFieldLoader $loader, $name) + { + foreach ($loader->getFields() as $field) { + if ($field->get('varname') === $name) { + return $field; + } + } + + throw new InvalidArgumentException("Found no configured field for '$name'"); + } + + /** + * @param IcingaObject $object + * @param $varName + * @return array + */ + protected function getCurrentInstances(IcingaObject $object, $varName) + { + $currentVars = $object->getVars(); + if (isset($currentVars->$varName)) { + $currentValue = $currentVars->$varName; + } else { + $currentValue = (object)[]; + } + if (is_object($currentValue)) { + $currentValue = (array)$currentValue; + } else { + throw new InvalidArgumentException(sprintf( + '"%s" is not a valid Dictionary', + json_encode($currentValue) + )); + } + return $currentValue; + } + + /** + * @param array $currentValue + * @param $subjects + * @return Hint|Table + */ + protected function prepareInstancesTable(array $currentValue) + { + $table = new Table(); + $table->addAttributes([ + 'class' => 'common-table table-row-selectable' + ]); + $table->getHeader()->add( + Table::row([ + $this->translate('Key / Instance'), + $this->translate('Properties') + ], ['style' => 'text-align: left'], 'th') + ); + foreach ($currentValue as $key => $item) { + $table->add(Table::row([ + Link::create($key, $this->url()->with('instance', $key)), + str_replace("\n", ' ', $this->shorten(PlainObjectRenderer::render($item), 512)) + ])); + } + + return $table; + } + + /** + * @param IcingaObject $object + * @param $varName + * @return string + */ + protected function prepareSubjectsLabel(IcingaObject $object, $varName) + { + if ($object instanceof IcingaService) { + $hostName = $object->get('host'); + $subjects = $object->getObjectName() . " ($varName)"; + } else { + $hostName = $object->getObjectName(); + $subjects = sprintf( + $this->translate('%s instances'), + $varName + ); + } + $this->addTitle(sprintf( + $this->translate('%s on %s'), + $subjects, + $hostName + )); + return $subjects; + } + + protected function addListActions(DirectorDatalist $list) + { + $this->actions()->add( + Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'DataList', + 'names' => $list->getUniqueIdentifier() + ], + ['class' => 'icon-tag'] + ) + ); + } + + /** + * @param $paramName + * @return DirectorDatalist + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireList($paramName) + { + return DirectorDatalist::load($this->params->getRequired($paramName), $this->db()); + } + + protected function addListTabs($name, $activate) + { + $this->tabs()->add('list', [ + 'url' => 'director/data/list', + 'urlParams' => ['name' => $name], + 'label' => $this->translate('Edit list'), + ])->add('entries', [ + 'url' => 'director/data/listentry', + 'urlParams' => ['list' => $name], + 'label' => $this->translate('List entries'), + ])->activate($activate); + + return $this; + } +} diff --git a/application/controllers/DatafieldController.php b/application/controllers/DatafieldController.php new file mode 100644 index 0000000..afad317 --- /dev/null +++ b/application/controllers/DatafieldController.php @@ -0,0 +1,40 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\DirectorDatafieldForm; +use Icinga\Module\Director\Web\Controller\ActionController; + +class DatafieldController extends ActionController +{ + public function addAction() + { + $this->indexAction(); + } + + public function editAction() + { + $this->indexAction(); + } + + public function indexAction() + { + $form = DirectorDatafieldForm::load() + ->setDb($this->db()); + + if ($id = $this->params->get('id')) { + $form->loadObject((int) $id); + $this->addTitle( + $this->translate('Modify %s'), + $form->getObject()->varname + ); + $this->addSingleTab($this->translate('Edit a Field')); + } else { + $this->addTitle($this->translate('Add a new Data Field')); + $this->addSingleTab($this->translate('New Field')); + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/DatafieldcategoryController.php b/application/controllers/DatafieldcategoryController.php new file mode 100644 index 0000000..32c76ef --- /dev/null +++ b/application/controllers/DatafieldcategoryController.php @@ -0,0 +1,46 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\DirectorDatafieldCategoryForm; +use Icinga\Module\Director\Web\Controller\ActionController; + +class DatafieldcategoryController extends ActionController +{ + public function addAction() + { + $this->indexAction(); + } + + public function editAction() + { + $this->indexAction(); + } + + public function indexAction() + { + $edit = false; + + if ($name = $this->params->get('name')) { + $edit = true; + } + + $form = DirectorDatafieldCategoryForm::load() + ->setDb($this->db()); + + if ($edit) { + $form->loadObject($name); + $this->addTitle( + $this->translate('Modify %s'), + $form->getObject()->category_name + ); + $this->addSingleTab($this->translate('Edit a Category')); + } else { + $this->addTitle($this->translate('Add a new Data Field Category')); + $this->addSingleTab($this->translate('New Category')); + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/DependenciesController.php b/application/controllers/DependenciesController.php new file mode 100644 index 0000000..276dd63 --- /dev/null +++ b/application/controllers/DependenciesController.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class DependenciesController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + return $res; + } +} diff --git a/application/controllers/DependencyController.php b/application/controllers/DependencyController.php new file mode 100644 index 0000000..9d21cd5 --- /dev/null +++ b/application/controllers/DependencyController.php @@ -0,0 +1,63 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaDependencyForm; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaDependency; + +class DependencyController extends ObjectController +{ + protected $apply; + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\NotFoundError + */ + public function init() + { + parent::init(); + + if ($apply = $this->params->get('apply')) { + $this->apply = IcingaDependency::load( + array('object_name' => $apply, 'object_type' => 'template'), + $this->db() + ); + } + } + + /** + * @return \Icinga\Module\Director\Objects\IcingaObject + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\InvalidPropertyException + * @throws \Icinga\Exception\NotFoundError + */ + protected function loadObject() + { + if ($this->object === null) { + if ($name = $this->params->get('name')) { + $params = array('object_name' => $name); + $db = $this->db(); + + $this->object = IcingaDependency::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } + + /** + * Hint: this is never being called. Why? + * + * @param $form + */ + protected function beforeHandlingAddRequest($form) + { + /** @var IcingaDependencyForm $form */ + if ($this->apply) { + $form->createApplyRuleFor($this->apply); + } + } +} diff --git a/application/controllers/DependencytemplateController.php b/application/controllers/DependencytemplateController.php new file mode 100644 index 0000000..e2bc49d --- /dev/null +++ b/application/controllers/DependencytemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaDependency; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class DependencytemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaDependency::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/DeploymentController.php b/application/controllers/DeploymentController.php new file mode 100644 index 0000000..2d35f3c --- /dev/null +++ b/application/controllers/DeploymentController.php @@ -0,0 +1,28 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\Web\Widget\DeploymentInfo; + +class DeploymentController extends ActionController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/deploy'); + } + + public function indexAction() + { + $info = new DeploymentInfo(DirectorDeploymentLog::load( + $this->params->get('id'), + $this->db() + )); + $this->addTitle($this->translate('Deployment details')); + $this->tabs( + $info->getTabs($this->getAuth(), $this->getRequest()) + )->activate('deployment'); + $this->content()->add($info); + } +} diff --git a/application/controllers/EndpointController.php b/application/controllers/EndpointController.php new file mode 100644 index 0000000..e8a4fb0 --- /dev/null +++ b/application/controllers/EndpointController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class EndpointController extends ObjectController +{ +} diff --git a/application/controllers/EndpointsController.php b/application/controllers/EndpointsController.php new file mode 100644 index 0000000..40501a4 --- /dev/null +++ b/application/controllers/EndpointsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class EndpointsController extends ObjectsController +{ +} diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php new file mode 100644 index 0000000..4fac4d2 --- /dev/null +++ b/application/controllers/HealthController.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Tabs\MainTabs; +use ipl\Html\Html; +use Icinga\Module\Director\Web\Widget\HealthCheckPluginOutput; +use Icinga\Module\Director\Health; +use Icinga\Module\Director\Web\Controller\ActionController; + +class HealthController extends ActionController +{ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('health'); + $this->setTitle($this->translate('Director Health')); + $health = new Health(); + $health->setDbResourceName($this->getDbResourceName()); + $output = new HealthCheckPluginOutput($health); + $this->content()->add($output); + $this->content()->add([ + Html::tag('h1', ['class' => 'icon-pin'], $this->translate('Hint: Check Plugin')), + Html::tag('p', $this->translate( + 'Did you know that you can run this entire Health Check' + . ' (or just some sections) as an Icinga Check on a regular' + . ' base?' + )) + ]); + } +} diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php new file mode 100644 index 0000000..e107d22 --- /dev/null +++ b/application/controllers/HostController.php @@ -0,0 +1,637 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Web\Table\ObjectsTableService; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Tabs; +use Exception; +use Icinga\Module\Director\CustomVariable\CustomVariableDictionary; +use Icinga\Module\Director\Db\AppliedServiceSetLoader; +use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder; +use Icinga\Module\Director\Forms\IcingaAddServiceForm; +use Icinga\Module\Director\Forms\IcingaServiceForm; +use Icinga\Module\Director\Forms\IcingaServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\SelfService; +use Icinga\Module\Director\Web\Table\IcingaHostAppliedForServiceTable; +use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable; + +class HostController extends ObjectController +{ + protected function checkDirectorPermissions() + { + if ($this->isServiceAction() && (new Monitoring())->authCanEditService( + $this->Auth(), + $this->getParam('name'), + $this->getParam('service') + )) { + return; + } + + if ($this->isServicesReadOnlyAction()) { + $this->assertPermission('director/monitoring/services-ro'); + return; + } + + if ($this->hasPermission('director/hosts')) { // faster + return; + } + + if ($this->canModifyHostViaMonitoringPermissions($this->getParam('name'))) { + return; + } + + $this->assertPermission('director/hosts'); // complain about default hosts permission + } + + protected function isServicesReadOnlyAction() + { + return in_array($this->getRequest()->getActionName(), [ + 'servicesro', + 'findservice', + 'invalidservice', + ]); + } + + protected function isServiceAction() + { + return in_array($this->getRequest()->getActionName(), [ + 'servicesro', + 'findservice', + 'invalidservice', + 'servicesetservice', + 'appliedservice', + 'inheritedservice', + ]); + } + + protected function canModifyHostViaMonitoringPermissions($hostname) + { + if ($this->hasPermission('director/monitoring/hosts')) { + $monitoring = new Monitoring(); + return $monitoring->authCanEditHost($this->Auth(), $hostname); + } + + return false; + } + + /** + * @return HostgroupRestriction + */ + protected function getHostgroupRestriction() + { + return new HostgroupRestriction($this->db(), $this->Auth()); + } + + public function editAction() + { + parent::editAction(); + $this->addOptionalMonitoringLink(); + } + + public function serviceAction() + { + $host = $this->getHostObject(); + $this->addServicesHeader(); + $this->addTitle($this->translate('Add Service to %s'), $host->getObjectName()); + $this->content()->add( + IcingaAddServiceForm::load() + ->setBranch($this->getBranch()) + ->setHost($host) + ->setDb($this->db()) + ->handleRequest() + ); + } + + public function servicesetAction() + { + $host = $this->getHostObject(); + $this->addServicesHeader(); + $this->addTitle($this->translate('Add Service Set to %s'), $host->getObjectName()); + + $this->content()->add( + IcingaServiceSetForm::load() + ->setBranch($this->getBranch()) + ->setHost($host) + ->setDb($this->db()) + ->handleRequest() + ); + } + + protected function addServicesHeader() + { + $host = $this->getHostObject(); + $hostname = $host->getObjectName(); + $this->tabs()->activate('services'); + + $this->actions()->add(Link::create( + $this->translate('Add service'), + 'director/host/service', + ['name' => $hostname], + ['class' => 'icon-plus'] + ))->add(Link::create( + $this->translate('Add service set'), + 'director/host/serviceset', + ['name' => $hostname], + ['class' => 'icon-plus'] + )); + } + + public function findserviceAction() + { + $host = $this->getHostObject(); + $this->redirectNow( + (new ServiceFinder($host, $this->getAuth())) + ->getRedirectionUrl($this->params->get('service')) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function invalidserviceAction() + { + if (! $this->showInfoForNonDirectorService()) { + $this->content()->add(Hint::error(sprintf( + $this->translate('No such service: %s'), + $this->params->get('service') + ))); + } + + $this->servicesAction(); + } + + protected function showInfoForNonDirectorService() + { + try { + $api = $this->getApiIfAvailable(); + if ($api) { + $name = $this->params->get('name') . '!' . $this->params->get('service'); + $info = $api->getObject($name, 'Services'); + if (isset($info->attrs->source_location)) { + $source = $info->attrs->source_location; + $this->content()->add(Hint::info(Html::sprintf( + 'The configuration for this object has not been rendered by' + . ' Icinga Director. You can find it on line %s in %s.', + Html::tag('strong', null, $source->first_line), + Html::tag('strong', null, $source->path) + ))); + } + } + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function servicesAction() + { + $this->addServicesHeader(); + $host = $this->getHostObject(); + $this->addTitle($this->translate('Services: %s'), $host->getObjectName()); + $branch = $this->getBranch(); + $hostHasBeenCreatedInBranch = $branch->isBranch() && $host->get('id'); + $content = $this->content(); + $table = (new ObjectsTableService($this->db())) + ->setAuth($this->Auth()) + ->setHost($host) + ->setBranch($branch) + ->setTitle($this->translate('Individual Service objects')) + ->removeQueryLimit(); + + if (count($table)) { + $content->add($table); + } + + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this->object) + ->getTemplatesFor($this->object, true); + foreach ($parents as $parent) { + $table = (new ObjectsTableService($this->db())) + ->setAuth($this->Auth()) + ->setBranch($branch) + ->setHost($parent) + ->setInheritedBy($host) + ->removeQueryLimit(); + + if (count($table)) { + $content->add( + $table->setTitle(sprintf( + $this->translate('Inherited from %s'), + $parent->getObjectName() + )) + ); + } + } + + if (! $hostHasBeenCreatedInBranch) { + $this->addHostServiceSetTables($host); + } + foreach ($parents as $parent) { + $this->addHostServiceSetTables($parent, $host); + } + + $appliedSets = AppliedServiceSetLoader::fetchForHost($host); + foreach ($appliedSets as $set) { + $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName()); + + $content->add( + IcingaServiceSetServiceTable::load($set) + // ->setHost($host) + ->setBranch($branch) + ->setAffectedHost($host) + ->setTitle($title) + ->removeQueryLimit() + ); + } + + $table = IcingaHostAppliedServicesTable::load($host) + ->setTitle($this->translate('Applied services')); + + if (count($table)) { + $content->add($table); + } + } + + /** + * Hint: this duplicates quite some logic from servicesAction. We might want + * to clean this up, but as soon as we store fully resolved Services this + * will be obsolete anyways + * + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + * @throws \Icinga\Exception\MissingParameterException + */ + public function servicesroAction() + { + $this->assertPermission('director/monitoring/services-ro'); + $host = $this->getHostObject(); + $service = $this->params->getRequired('service'); + $db = $this->db(); + $branch = $this->getBranch(); + $this->controls()->setTabs(new Tabs()); + $this->addSingleTab($this->translate('Configuration (read-only)')); + $this->addTitle($this->translate('Services on %s'), $host->getObjectName()); + $content = $this->content(); + + $table = (new ObjectsTableService($db)) + ->setAuth($this->Auth()) + ->setHost($host) + ->setBranch($branch) + ->setReadonly() + ->highlightService($service) + ->setTitle($this->translate('Individual Service objects')); + + if (count($table)) { + $content->add($table); + } + + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this->object) + ->getTemplatesFor($this->object, true); + foreach ($parents as $parent) { + $table = (new ObjectsTableService($db)) + ->setReadonly() + ->setBranch($branch) + ->setHost($parent) + ->highlightService($service) + ->setInheritedBy($host); + if (count($table)) { + $content->add( + $table->setTitle(sprintf( + 'Inherited from %s', + $parent->getObjectName() + )) + ); + } + } + + $this->addHostServiceSetTables($host); + foreach ($parents as $parent) { + $this->addHostServiceSetTables($parent, $host, $service); + } + + $appliedSets = AppliedServiceSetLoader::fetchForHost($host); + foreach ($appliedSets as $set) { + $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName()); + + $content->add( + IcingaServiceSetServiceTable::load($set) + // ->setHost($host) + ->setBranch($branch) + ->setAffectedHost($host) + ->setReadonly() + ->highlightService($service) + ->setTitle($title) + ); + } + + $table = IcingaHostAppliedServicesTable::load($host) + ->setReadonly() + ->highlightService($service) + ->setTitle($this->translate('Applied services')); + + if (count($table)) { + $content->add($table); + } + } + + /** + * @param IcingaHost $host + * @param IcingaHost|null $affectedHost + */ + protected function addHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null, $roService = null) + { + $db = $this->db(); + if ($affectedHost === null) { + $affectedHost = $host; + } + if ($host->get('id') === null) { + return; + } + + $query = $db->getDbAdapter()->select() + ->from( + array('ss' => 'icinga_service_set'), + 'ss.*' + )->join( + array('hsi' => 'icinga_service_set_inheritance'), + 'hsi.parent_service_set_id = ss.id', + array() + )->join( + array('hs' => 'icinga_service_set'), + 'hs.id = hsi.service_set_id', + array() + )->where('hs.host_id = ?', $host->get('id')); + + $sets = IcingaServiceSet::loadAll($db, $query, 'object_name'); + /** @var IcingaServiceSet $set*/ + foreach ($sets as $name => $set) { + $title = sprintf($this->translate('%s (Service set)'), $name); + $table = IcingaServiceSetServiceTable::load($set) + ->setHost($host) + ->setBranch($this->getBranch()) + ->setAffectedHost($affectedHost) + ->setTitle($title); + if ($roService) { + $table->setReadonly()->highlightService($roService); + } + $this->content()->add($table); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function appliedserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceId = $this->params->get('service_id'); + $parent = IcingaService::loadWithAutoIncId($serviceId, $db); + $serviceName = $parent->getObjectName(); + + $service = IcingaService::create([ + 'imports' => $parent, + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + $this->addTitle( + $this->translate('Applied service: %s'), + $serviceName + ); + + $this->content()->add( + IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setApplyGenerated($parent) + ->setObject($service) + ->handleRequest() + ); + + $this->commonForServices(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function inheritedserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceName = $this->params->get('service'); + $from = IcingaHost::load($this->params->get('inheritedFrom'), $this->db()); + + $parent = IcingaService::load([ + 'object_name' => $serviceName, + 'host_id' => $from->get('id') + ], $this->db()); + + // TODO: we want to eventually show the host template name, doesn't work + // as template resolution would break. + // $parent->object_name = $from->object_name; + + $service = IcingaService::create([ + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'imports' => [$parent], + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + $this->addTitle($this->translate('Inherited service: %s'), $serviceName); + + $form = IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setInheritedFrom($from->getObjectName()) + ->setObject($service) + ->handleRequest(); + $this->content()->add($form); + $this->commonForServices(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function removesetAction() + { + // TODO: clean this up, use POST + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from( + array('ss' => 'icinga_service_set'), + array('id' => 'ss.id') + )->join( + array('si' => 'icinga_service_set_inheritance'), + 'si.service_set_id = ss.id', + array() + )->where( + 'si.parent_service_set_id = ?', + $this->params->get('setId') + )->where('ss.host_id = ?', $this->object->get('id')); + + IcingaServiceSet::loadWithAutoIncId($db->fetchOne($query), $this->db())->delete(); + $this->redirectNow( + Url::fromPath('director/host/services', array( + 'name' => $this->object->getObjectName() + )) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function servicesetserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceName = $this->params->get('service'); + $setParams = [ + 'object_name' => $this->params->get('set'), + 'host_id' => $host->get('id') + ]; + $setTemplate = IcingaServiceSet::load($this->params->get('set'), $db); + if (IcingaServiceSet::exists($setParams, $db)) { + $set = IcingaServiceSet::load($setParams, $db); + } else { + $set = $setTemplate; + } + + $service = IcingaService::load([ + 'object_name' => $serviceName, + 'service_set_id' => $setTemplate->get('id') + ], $this->db()); + $service = IcingaService::create([ + 'id' => $service->get('id'), + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'imports' => $service->listImportNames(), + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + // $set->copyVarsToService($service); + $this->addTitle( + $this->translate('%s on %s (from set: %s)'), + $serviceName, + $host->getObjectName(), + $set->getObjectName() + ); + + $form = IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setServiceSet($set) + ->setObject($service) + ->handleRequest(); + $this->tabs()->activate('services'); + $this->content()->add($form); + $this->commonForServices(); + } + + protected function commonForServices() + { + $host = $this->object; + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/host/services', + ['name' => $host->getObjectName()], + ['class' => 'icon-left-big'] + )); + $this->tabs()->activate('services'); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function agentAction() + { + $selfService = new SelfService($this->getHostObject(), $this->api()); + if ($os = $this->params->get('download')) { + $selfService->handleLegacyAgentDownloads($os); + return; + } + + $selfService->renderTo($this); + $this->tabs()->activate('agent'); + } + + protected function addOptionalMonitoringLink() + { + $host = $this->object; + try { + $mon = $this->monitoring(); + if ($host->isObject() + && $mon->isAvailable() + && $mon->hasHost($host->getObjectName()) + ) { + $this->actions()->add(Link::create( + $this->translate('Show'), + 'monitoring/host/show', + ['host' => $host->getObjectName()], + [ + 'class' => 'icon-globe critical', + 'data-base-target' => '_next' + ] + )); + + // Intentionally placed here, show it only for deployed Hosts + $this->addOptionalInspectLink(); + } + } catch (Exception $e) { + // Silently ignore errors in the monitoring module + } + } + + protected function addOptionalInspectLink() + { + if (! $this->hasPermission('director/inspect')) { + return; + } + + $this->actions()->add(Link::create( + $this->translate('Inspect'), + 'director/inspect/object', + [ + 'type' => 'host', + 'plural' => 'hosts', + 'name' => $this->object->getObjectName() + ], + [ + 'class' => 'icon-zoom-in', + 'data-base-target' => '_next' + ] + )); + } + + /** + * @return IcingaHost + */ + protected function getHostObject() + { + assert($this->object instanceof IcingaHost); + return $this->object; + } +} diff --git a/application/controllers/HostgroupController.php b/application/controllers/HostgroupController.php new file mode 100644 index 0000000..aa4cc51 --- /dev/null +++ b/application/controllers/HostgroupController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class HostgroupController extends ObjectController +{ +} diff --git a/application/controllers/HostgroupsController.php b/application/controllers/HostgroupsController.php new file mode 100644 index 0000000..2b4b417 --- /dev/null +++ b/application/controllers/HostgroupsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class HostgroupsController extends ObjectsController +{ +} diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php new file mode 100644 index 0000000..0332072 --- /dev/null +++ b/application/controllers/HostsController.php @@ -0,0 +1,138 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Url; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Forms\IcingaAddServiceForm; +use Icinga\Module\Director\Forms\IcingaAddServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Controller\ObjectsController; +use gipfl\IcingaWeb2\Link; + +class HostsController extends ObjectsController +{ + protected $multiEdit = array( + 'imports', + 'groups', + 'disabled' + ); + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/hosts'); + } + + public function editAction() + { + $url = clone($this->getRequest()->getUrl()); + $url->setPath('director/hosts/addservice'); + + $urlSet = clone($url); + $urlSet->setPath('director/hosts/addserviceset'); + + parent::editAction(); + + $this->actions()->add(Link::create( + $this->translate('Add Service'), + $url, + null, + ['class' => 'icon-plus'] + ))->add(Link::create( + $this->translate('Add Service Set'), + $urlSet, + null, + ['class' => 'icon-plus'] + )); + } + + public function edittemplatesAction() + { + parent::editAction(); + + $objects = $this->loadMultiObjectsFromParams(); + $names = []; + /** @var ExportInterface $object */ + foreach ($objects as $object) { + $names[] = $object->getUniqueIdentifier(); + } + + $url = Url::fromPath('director/basket/add', [ + 'type' => 'HostTemplate', + ]); + + $url->getParams()->addValues('names', $names); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + $url, + null, + ['class' => 'icon-tag'] + )); + } + + public function addserviceAction() + { + $this->addSingleTab($this->translate('Add Service')); + $filter = Filter::fromQueryString($this->params->toString()); + + $objects = array(); + $db = $this->db(); + /** @var $filter FilterChain */ + foreach ($filter->filters() as $sub) { + /** @var $sub FilterChain */ + foreach ($sub->filters() as $ex) { + /** @var $ex FilterChain|FilterExpression */ + if ($ex->isExpression() && $ex->getColumn() === 'name') { + $name = $ex->getExpression(); + $objects[$name] = IcingaHost::load($name, $db); + } + } + } + $this->addTitle( + $this->translate('Add service to %d hosts'), + count($objects) + ); + + $this->content()->add( + IcingaAddServiceForm::load() + ->setHosts($objects) + ->setDb($this->db()) + ->handleRequest() + ); + } + + public function addservicesetAction() + { + $this->addSingleTab($this->translate('Add Service Set')); + $filter = Filter::fromQueryString($this->params->toString()); + + $objects = array(); + $db = $this->db(); + /** @var $filter FilterChain */ + foreach ($filter->filters() as $sub) { + /** @var $sub FilterChain */ + foreach ($sub->filters() as $ex) { + /** @var $ex FilterChain|FilterExpression */ + if ($ex->isExpression() && $ex->getColumn() === 'name') { + $name = $ex->getExpression(); + $objects[$name] = IcingaHost::load($name, $db); + } + } + } + $this->addTitle( + $this->translate('Add Service Set to %d hosts'), + count($objects) + ); + + $this->content()->add( + IcingaAddServiceSetForm::load() + ->setHosts($objects) + ->setDb($this->db()) + ->handleRequest() + ); + } +} diff --git a/application/controllers/HosttemplateController.php b/application/controllers/HosttemplateController.php new file mode 100644 index 0000000..a5bfc2b --- /dev/null +++ b/application/controllers/HosttemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class HosttemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaHost::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/ImportrunController.php b/application/controllers/ImportrunController.php new file mode 100644 index 0000000..d0e34e5 --- /dev/null +++ b/application/controllers/ImportrunController.php @@ -0,0 +1,24 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\ImportRun; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\ImportedrowsTable; + +class ImportrunController extends ActionController +{ + public function indexAction() + { + $importRun = ImportRun::load($this->params->getRequired('id'), $this->db()); + $this->addTitle($this->translate('Import run')); + $this->addSingleTab($this->translate('Import run')); + + $table = ImportedrowsTable::load($importRun); + if ($chosen = $this->params->get('chosenColumns')) { + $table->setColumns(preg_split('/,/', $chosen, -1, PREG_SPLIT_NO_EMPTY)); + } + + $table->renderTo($this); + } +} diff --git a/application/controllers/ImportsourceController.php b/application/controllers/ImportsourceController.php new file mode 100644 index 0000000..cbddb9e --- /dev/null +++ b/application/controllers/ImportsourceController.php @@ -0,0 +1,375 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Forms\ImportRowModifierForm; +use Icinga\Module\Director\Forms\ImportSourceForm; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Form\CloneImportSourceForm; +use Icinga\Module\Director\Web\Table\ImportrunTable; +use Icinga\Module\Director\Web\Table\ImportsourceHookTable; +use Icinga\Module\Director\Web\Table\PropertymodifierTable; +use Icinga\Module\Director\Web\Tabs\ImportsourceTabs; +use Icinga\Module\Director\Web\Widget\ImportSourceDetails; +use InvalidArgumentException; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Error; +use ipl\Html\Html; + +class ImportsourceController extends ActionController +{ + use BranchHelper; + + /** @var ImportSource|null */ + private $importSource; + + private $id; + + /** + * @throws \Icinga\Exception\AuthenticationException + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function init() + { + parent::init(); + $id = $this->params->get('source_id', $this->params->get('id')); + if ($id !== null && is_numeric($id)) { + $this->id = (int) $id; + } + + $tabs = $this->tabs(new ImportsourceTabs($this->id)); + $action = $this->getRequest()->getActionName(); + if ($tabs->has($action)) { + $tabs->activate($action); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + protected function addMainActions() + { + $this->actions(new AutomationObjectActionBar( + $this->getRequest() + )); + $source = $this->getImportSource(); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'ImportSource', + 'names' => $source->getUniqueIdentifier() + ], + [ + 'class' => 'icon-tag', + 'data-base-target' => '_next' + ] + )); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->addMainActions(); + $source = $this->getImportSource(); + if ($this->params->get('format') === 'json') { + $this->sendJson($this->getResponse(), (new Exporter($this->db()))->export($source)); + return; + } + $this->addTitle( + $this->translate('Import source: %s'), + $source->get('source_name') + )->setAutorefreshInterval(10); + $branch = $this->getBranch(); + if ($this->getBranch()->isBranch()) { + $this->content()->add(Hint::info(Html::sprintf($this->translate( + 'Please note that importing data will take place in your main Branch.' + . ' Modifications to Import Sources are not allowed while being in a Configuration Branch.' + . ' To get the full functionality, please deactivate %s' + ), Branch::requireHook()->linkToBranch($branch, $this->getAuth(), $branch->getName())))); + } + $this->content()->add(new ImportSourceDetails($source)); + } + + public function addAction() + { + $this->addTitle($this->translate('Add import source')); + if ($this->showNotInBranch($this->translate('Creating Import Sources'))) { + return; + } + + $this->content()->add( + ImportSourceForm::load()->setDb($this->db()) + ->setSuccessUrl('director/importsources') + ->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $this->addMainActions(); + $this->activateTabWithPostfix($this->translate('Modify')); + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + $form = ImportSourceForm::load() + ->setObject($this->getImportSource()) + ->setListUrl('director/importsources') + ->handleRequest(); + $this->addTitle( + $this->translate('Import source: %s'), + $form->getObject()->get('source_name') + )->setAutorefreshInterval(10); + + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function cloneAction() + { + $this->addMainActions(); + $this->activateTabWithPostfix($this->translate('Clone')); + if ($this->showNotInBranch($this->translate('Cloning Import Sources'))) { + return; + } + $source = $this->getImportSource(); + $this->addTitle('Clone: %s', $source->get('source_name')); + $form = new CloneImportSourceForm($source); + $this->content()->add($form); + $form->on(CloneImportSourceForm::ON_SUCCESS, function (CloneImportSourceForm $form) { + $this->getResponse()->redirectAndExit($form->getSuccessUrl()); + }); + $form->handleRequest($this->getServerRequest()); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function previewAction() + { + $source = $this->getImportSource(); + + $this->addTitle( + $this->translate('Import source preview: %s'), + $source->get('source_name') + ); + $fetchUrl = clone($this->url()); + + $this->actions()->add(Link::create( + $this->translate('Download JSON'), + $fetchUrl->setPath('director/importsource/fetch'), + null, + [ + 'target' => '_blank', + 'class' => 'icon-download', + ] + ))->add(Link::create('[..]', '#', null, [ + 'onclick' => 'javascript:$("table.raw-data-table").toggleClass("collapsed");' + ])); + try { + (new ImportsourceHookTable())->setImportSource($source)->renderTo($this); + } catch (Exception $e) { + $this->content()->add(Error::show($e)); + } + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function fetchAction() + { + $response = $this->getResponse(); + try { + $source = $this->getImportSource(); + $source->checkForChanges(); + $hook = ImportSourceHook::forImportSource($source); + $data = $hook->fetchData(); + $source->applyModifiers($data); + + $filename = sprintf( + "director-importsource-%d_%s.json", + $this->getParam('id'), + date('YmdHis') + ); + $response->setHeader('Content-Type', 'application/json', true); + $response->setHeader('Content-disposition', "attachment; filename=$filename", true); + $response->sendHeaders(); + $this->sendJson($this->getResponse(), $data); + } catch (Exception $e) { + $this->sendJsonError($response, $e->getMessage()); + } + // TODO: this is not clean + if (\ob_get_level()) { + \ob_end_flush(); + } + exit; + } + + /** + * @return ImportSource + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireImportSourceAndAddModifierTable() + { + $source = $this->getImportSource(); + $table = PropertymodifierTable::load($source, $this->url()); + if ($this->getBranch()->isBranch()) { + $table->setReadOnly(); + } else { + $table->handleSortPriorityActions($this->getRequest(), $this->getResponse()); + } + $table->renderTo($this); + + return $source; + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function modifierAction() + { + $source = $this->requireImportSourceAndAddModifierTable(); + $this->addTitle($this->translate('Property modifiers: %s'), $source->get('source_name')); + $this->addAddLink( + $this->translate('Add property modifier'), + 'director/importsource/addmodifier', + ['source_id' => $source->get('id')], + '_self' + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function historyAction() + { + $source = $this->getImportSource(); + $this->addTitle($this->translate('Import run history: %s'), $source->get('source_name')); + + // TODO: temporarily disabled, find a better place for stats: + // $this->view->stats = $this->db()->fetchImportStatistics(); + ImportrunTable::load($source)->renderTo($this); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function addmodifierAction() + { + $source = $this->requireImportSourceAndAddModifierTable(); + $this->addTitle( + $this->translate('%s: add Property Modifier'), + $source->get('source_name') + )->addBackToModifiersLink($source); + $this->tabs()->activate('modifier'); + + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + + $this->content()->prepend( + ImportRowModifierForm::load()->setDb($this->db()) + ->setSource($source) + ->setSuccessUrl( + 'director/importsource/modifier', + ['source_id' => $source->get('id')] + )->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function editmodifierAction() + { + // We need to load the table AFTER adding the title, otherwise search + // will not be placed next to the title + $source = $this->getImportSource(); + + $this->addTitle( + $this->translate('%s: Property Modifier'), + $source->get('source_name') + )->addBackToModifiersLink($source); + $source = $this->requireImportSourceAndAddModifierTable(); + $this->tabs()->activate('modifier'); + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + + $listUrl = 'director/importsource/modifier?source_id=' + . (int) $source->get('id'); + $this->content()->prepend( + ImportRowModifierForm::load()->setDb($this->db()) + ->loadObject((int) $this->params->getRequired('id')) + ->setListUrl($listUrl) + ->setSource($source) + ->handleRequest() + ); + } + + /** + * @return ImportSource + * @throws \Icinga\Exception\NotFoundError + */ + protected function getImportSource() + { + if ($this->importSource === null) { + if ($this->id === null) { + throw new InvalidArgumentException('Got no ImportSource id'); + } + $this->importSource = ImportSource::loadWithAutoIncId( + $this->id, + $this->db() + ); + } + + return $this->importSource; + } + + protected function activateTabWithPostfix($title) + { + /** @var ImportsourceTabs $tabs */ + $tabs = $this->tabs(); + $tabs->activateMainWithPostfix($title); + + return $this; + } + + /** + * @param ImportSource $source + * @return $this + */ + protected function addBackToModifiersLink(ImportSource $source) + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/importsource/modifier', + ['source_id' => $source->get('id')], + ['class' => 'icon-left-big'] + ) + ); + + return $this; + } +} diff --git a/application/controllers/ImportsourcesController.php b/application/controllers/ImportsourcesController.php new file mode 100644 index 0000000..4287292 --- /dev/null +++ b/application/controllers/ImportsourcesController.php @@ -0,0 +1,57 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\DirectorObject\Automation\ImportExport; +use Icinga\Module\Director\Web\Table\ImportsourceTable; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Tabs\ImportTabs; + +class ImportsourcesController extends ActionController +{ + protected $isApified = true; + + public function indexAction() + { + if ($this->getRequest()->isApiRequest()) { + switch (strtolower($this->getRequest()->getMethod())) { + case 'get': + $this->sendExport(); + break; + case 'post': + $this->acceptImport($this->getRequest()->getRawBody()); + break; + // TODO: put / replace all? + default: + $this->sendUnsupportedMethod(); + } + + return; + } + + $this->addTitle($this->translate('Import source')) + ->setAutoRefreshInterval(10) + ->addAddLink( + $this->translate('Add a new Import Source'), + 'director/importsource/add' + )->tabs(new ImportTabs())->activate('importsource'); + + (new ImportsourceTable($this->db()))->renderTo($this); + } + + /** + * @param $raw + */ + protected function acceptImport($raw) + { + (new ImportExport($this->db()))->unserializeImportSources(json_decode($raw)); + } + + protected function sendExport() + { + $this->sendJson( + $this->getResponse(), + (new ImportExport($this->db()))->serializeAllImportSources() + ); + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..3f6c62e --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,79 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Db\Migrations; +use Icinga\Module\Director\Forms\ApplyMigrationsForm; +use Icinga\Module\Director\Forms\KickstartForm; +use ipl\Html\Html; + +class IndexController extends DashboardController +{ + protected $hasDeploymentEndpoint; + + public function indexAction() + { + if ($this->Config()->get('db', 'resource')) { + $migrations = new Migrations($this->db()); + + if ($migrations->hasSchema()) { + if (!$this->hasDeploymentEndpoint()) { + $this->showKickstartForm(); + } + } + + if ($migrations->hasPendingMigrations()) { + $this->content()->prepend( + ApplyMigrationsForm::load() + ->setMigrations($migrations) + ->handleRequest() + ); + } elseif ($migrations->hasBeenDowngraded()) { + $this->content()->add(Hint::warning(sprintf($this->translate( + 'Your DB schema (migration #%d) is newer than your code base.' + . ' Downgrading Icinga Director is not supported and might' + . ' lead to unexpected problems.' + ), $migrations->getLastMigrationNumber()))); + } + + if ($migrations->hasSchema()) { + parent::indexAction(); + } else { + $this->addTitle(sprintf( + $this->translate('Icinga Director Setup: %s'), + $this->translate('Create Schema') + )); + $this->addSingleTab('Setup'); + } + } else { + $this->addTitle(sprintf( + $this->translate('Icinga Director Setup: %s'), + $this->translate('Choose DB Resource') + )); + $this->addSingleTab('Setup'); + $this->showKickstartForm(); + } + } + + protected function showKickstartForm() + { + $form = KickstartForm::load(); + if ($name = $this->getPreferredDbResourceName()) { + $form->setDbResourceName($name); + } + $this->content()->prepend($form->handleRequest()); + } + + protected function hasDeploymentEndpoint() + { + try { + $this->hasDeploymentEndpoint = $this->db()->hasDeploymentEndpoint(); + } catch (Exception $e) { + return false; + } + + return $this->hasDeploymentEndpoint; + } +} diff --git a/application/controllers/InspectController.php b/application/controllers/InspectController.php new file mode 100644 index 0000000..d631652 --- /dev/null +++ b/application/controllers/InspectController.php @@ -0,0 +1,200 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\CoreApiFieldsTable; +use Icinga\Module\Director\Web\Table\CoreApiObjectsTable; +use Icinga\Module\Director\Web\Table\CoreApiPrototypesTable; +use Icinga\Module\Director\Web\Tabs\ObjectTabs; +use Icinga\Module\Director\Web\Tree\InspectTreeRenderer; +use Icinga\Module\Director\Web\Widget\IcingaObjectInspection; +use Icinga\Module\Director\Web\Widget\InspectPackages; +use ipl\Html\Html; + +class InspectController extends ActionController +{ + private $endpoint; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/inspect'); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function typesAction() + { + $object = $this->endpoint(); + $name = $object->getObjectName(); + $this->tabs( + new ObjectTabs('endpoint', $this->Auth(), $object) + )->activate('inspect'); + + $this->addTitle($this->translate('Icinga 2 - Objects: %s'), $name); + + $this->actions()->add( + Link::create( + $this->translate('Status'), + 'director/inspect/status', + ['endpoint' => $name], + [ + 'class' => 'icon-eye', + 'data-base-target' => '_next' + ] + ) + ); + $this->content()->add( + new InspectTreeRenderer($object) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function typeAction() + { + $api = $this->endpoint()->api(); + $typeName = $this->params->get('type'); + $this->addSingleTab($this->translate('Inspect - object list')); + $this->addTitle( + $this->translate('Object type "%s"'), + $typeName + ); + $c = $this->content(); + $type = $api->getType($typeName); + if ($type->abstract) { + $c->add($this->translate('This is an abstract object type.')); + } + + if (! $type->abstract) { + $objects = $api->listObjects($typeName, $type->plural_name); + $c->add(Html::tag('p', null, sprintf($this->translate('%d objects found'), count($objects)))); + $c->add(new CoreApiObjectsTable($objects, $this->endpoint(), $type)); + } + + if (count((array) $type->fields)) { + $c->add([ + Html::tag('h2', null, $this->translate('Type attributes')), + new CoreApiFieldsTable($type->fields, $this->url()) + ]); + } + + if (count($type->prototype_keys)) { + $c->add([ + Html::tag('h2', null, $this->translate('Prototypes (methods)')), + new CoreApiPrototypesTable($type->prototype_keys, $type->name) + ]); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function objectAction() + { + $name = $this->params->get('name'); + $pType = $this->params->get('plural'); + $this->addSingleTab($this->translate('Object Inspection')); + $this->addTitle('%s "%s"', $pType, $name); + $this->showEndpointInformation($this->endpoint()); + $this->content()->add( + new IcingaObjectInspection( + $this->endpoint()->api()->getObject($name, $pType), + $this->db() + ) + ); + } + + /** + * @param IcingaEndpoint $endpoint + */ + protected function showEndpointInformation(IcingaEndpoint $endpoint) + { + $this->content()->add( + Html::tag('p', null, Html::sprintf( + 'Inspected via %s (%s)', + $this->linkToEndpoint($endpoint), + $endpoint->getDescriptiveUrl() + )) + ); + } + + /** + * @param IcingaEndpoint $endpoint + * @return Link + */ + protected function linkToEndpoint(IcingaEndpoint $endpoint) + { + return Link::create($endpoint->getObjectName(), 'director/endpoint', [ + 'name' => $endpoint->getObjectName() + ]); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function statusAction() + { + $this->addSingleTab($this->translate('Status')); + $this->addTitle($this->translate('Icinga 2 API - Status')); + $this->content()->add(Html::tag( + 'pre', + null, + PlainObjectRenderer::render($this->endpoint()->api()->getStatus()) + )); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function packagesAction() + { + $db = $this->db(); + $endpointName = $this->params->get('endpoint'); + $package = $this->params->get('package'); + $stage = $this->params->get('stage'); + $file = $this->params->get('file'); + if ($endpointName === null) { + $endpoint = null; + } else { + $endpoint = IcingaEndpoint::load($endpointName, $db); + } + if ($endpoint === null) { + $this->addSingleTab($this->translate('Inspect Packages')); + } elseif ($file !== null) { + $this->addSingleTab($this->translate('Inspect File Content')); + } else { + $this->tabs( + new ObjectTabs('endpoint', $this->Auth(), $endpoint) + )->activate('packages'); + } + $widget = new InspectPackages($this->db(), 'director/inspect/packages'); + $this->addTitle($widget->getTitle($endpoint, $package, $stage, $file)); + if ($file === null) { + $this->actions()->add($widget->getBreadCrumb($endpoint, $package, $stage)); + } + $this->content()->add($widget->getContent($endpoint, $package, $stage, $file)); + } + + /** + * @return IcingaEndpoint + * @throws \Icinga\Exception\NotFoundError + */ + protected function endpoint() + { + if ($this->endpoint === null) { + if ($name = $this->params->get('endpoint')) { + $this->endpoint = IcingaEndpoint::load($name, $this->db()); + } else { + $this->endpoint = $this->db()->getDeploymentEndpoint(); + } + } + + return $this->endpoint; + } +} diff --git a/application/controllers/JobController.php b/application/controllers/JobController.php new file mode 100644 index 0000000..278c96b --- /dev/null +++ b/application/controllers/JobController.php @@ -0,0 +1,117 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Forms\DirectorJobForm; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\DirectorJob; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Widget\JobDetails; + +class JobController extends ActionController +{ + use BranchHelper; + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $job = $this->requireJob(); + $this + ->addJobTabs($job, 'show') + ->addTitle($this->translate('Job: %s'), $job->get('job_name')) + ->addToBasketLink() + ->content()->add(new JobDetails($job)); + } + + public function addAction() + { + $this + ->addSingleTab($this->translate('New Job')) + ->addTitle($this->translate('Add a new Job')); + if ($this->showNotInBranch($this->translate('Creating Jobs'))) { + return; + } + + $this->content()->add( + DirectorJobForm::load() + ->setSuccessUrl('director/job') + ->setDb($this->db()) + ->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $job = $this->requireJob(); + $this + ->addJobTabs($job, 'edit') + ->addTitle($this->translate('Job: %s'), $job->get('job_name')) + ->addToBasketLink(); + if ($this->showNotInBranch($this->translate('Modifying Jobs'))) { + return; + } + + $form = DirectorJobForm::load() + ->setListUrl('director/jobs') + ->setObject($job) + ->handleRequest(); + $this->content()->add($form); + } + + /** + * @return DirectorJob + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\MissingParameterException + */ + protected function requireJob() + { + return DirectorJob::loadWithAutoIncId((int) $this->params->getRequired('id'), $this->db()); + } + + /** + * @return $this + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function addToBasketLink() + { + $job = $this->requireJob(); + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'DirectorJob', + 'names' => $job->getUniqueIdentifier() + ], + ['class' => 'icon-tag'] + )); + + return $this; + } + + protected function addJobTabs(DirectorJob $job, $active) + { + $id = $job->get('id'); + + $this->tabs()->add('show', [ + 'url' => 'director/job', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Job'), + ])->add('edit', [ + 'url' => 'director/job/edit', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Config'), + ])->activate($active); + + return $this; + } +} diff --git a/application/controllers/JobsController.php b/application/controllers/JobsController.php new file mode 100644 index 0000000..11e86ed --- /dev/null +++ b/application/controllers/JobsController.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\JobTable; +use Icinga\Module\Director\Web\Tabs\ImportTabs; + +class JobsController extends ActionController +{ + public function indexAction() + { + $this->addTitle($this->translate('Jobs')) + ->setAutoRefreshInterval(10) + ->addAddLink($this->translate('Add a new Job'), 'director/job/add') + ->tabs(new ImportTabs())->activate('jobs'); + + (new JobTable($this->db()))->renderTo($this); + } +} diff --git a/application/controllers/KickstartController.php b/application/controllers/KickstartController.php new file mode 100644 index 0000000..99cde1b --- /dev/null +++ b/application/controllers/KickstartController.php @@ -0,0 +1,30 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Module\Director\Forms\KickstartForm; +use Icinga\Module\Director\Web\Controller\BranchHelper; + +class KickstartController extends DashboardController +{ + use BranchHelper; + + public function indexAction() + { + $this->addSingleTab($this->translate('Kickstart')) + ->addTitle($this->translate('Director Kickstart Wizard')); + if ($this->showNotInBranch($this->translate('Kickstart'))) { + return; + } + $form = KickstartForm::load(); + try { + $form->setEndpoint($this->db()->getDeploymentEndpoint()); + } catch (Exception $e) { + // Silently ignore DB errors + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/NotificationController.php b/application/controllers/NotificationController.php new file mode 100644 index 0000000..97fa0f4 --- /dev/null +++ b/application/controllers/NotificationController.php @@ -0,0 +1,85 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaNotification; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; + +class NotificationController extends ObjectController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/notifications'); + } + + // TODO: KILL IT + public function init() + { + parent::init(); + // TODO: Check if this is still needed, remove it otherwise + /** @var \Icinga\Web\Widget\Tab $tab */ + if ($this->object && $this->object->object_type === 'apply') { + if ($host = $this->params->get('host')) { + foreach ($this->getTabs()->getTabs() as $tab) { + $tab->getUrl()->setParam('host', $host); + } + } + + if ($service = $this->params->get('service')) { + foreach ($this->getTabs()->getTabs() as $tab) { + $tab->getUrl()->setParam('service', $service); + } + } + } + } + + /** + * @param DirectorObjectForm $form + */ + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if (! $this->object) { + return; + } + + if ($this->object->isTemplate()) { + $form->setListUrl('director/notifications/templates'); + } else { + $form->setListUrl('director/notifications/applyrules'); + } + } + + protected function hasBasketSupport() + { + return $this->object->isTemplate() || $this->object->isApplyRule(); + } + + protected function loadObject() + { + if ($this->object === null) { + if ($name = $this->params->get('name')) { + $params = array('object_name' => $name); + $db = $this->db(); + + if ($hostname = $this->params->get('host')) { + $this->view->host = IcingaHost::load($hostname, $db); + $params['host_id'] = $this->view->host->id; + } + + if ($service = $this->params->get('service')) { + $this->view->service = IcingaService::load($service, $db); + $params['service_id'] = $this->view->service->id; + } + + $this->object = IcingaNotification::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } +} diff --git a/application/controllers/NotificationsController.php b/application/controllers/NotificationsController.php new file mode 100644 index 0000000..2ddb360 --- /dev/null +++ b/application/controllers/NotificationsController.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class NotificationsController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + return $res; + } + + public function indexAction() + { + throw new NotFoundError('Not found'); + } + + protected function assertApplyRulePermission() + { + return $this->assertPermission('director/notifications'); + } + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/notifications'); + } +} diff --git a/application/controllers/NotificationtemplateController.php b/application/controllers/NotificationtemplateController.php new file mode 100644 index 0000000..0b8602c --- /dev/null +++ b/application/controllers/NotificationtemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaNotification; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class NotificationtemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaNotification::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/PhperrorController.php b/application/controllers/PhperrorController.php new file mode 100644 index 0000000..40a32c1 --- /dev/null +++ b/application/controllers/PhperrorController.php @@ -0,0 +1,43 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Module\Director\Application\DependencyChecker; +use Icinga\Module\Director\Web\Table\Dependency\DependencyInfoTable; +use Icinga\Web\Controller; + +class PhperrorController extends Controller +{ + public function errorAction() + { + $this->getTabs()->add('error', array( + 'label' => $this->translate('Error'), + 'url' => $this->getRequest()->getUrl() + ))->activate('error'); + $msg = $this->translate( + "PHP version 5.4.x is required for Director >= 1.4.0, you're running %s." + . ' Please either upgrade PHP or downgrade Icinga Director' + ); + $this->view->title = $this->translate('Unsatisfied dependencies'); + $this->view->message = sprintf($msg, PHP_VERSION); + } + + public function dependenciesAction() + { + $checker = new DependencyChecker(Icinga::app()); + if ($checker->satisfiesDependencies($this->Module())) { + $this->redirectNow('director'); + } + $this->setAutorefreshInterval(15); + $this->getTabs()->add('error', [ + 'label' => $this->translate('Error'), + 'url' => $this->getRequest()->getUrl() + ])->activate('error'); + $this->view->title = $this->translate('Unsatisfied dependencies'); + $this->view->table = (new DependencyInfoTable($checker, $this->Module()))->render(); + $this->view->message = $this->translate( + "Icinga Director depends on the following modules, please install/upgrade as required" + ); + } +} diff --git a/application/controllers/ScheduledDowntimeController.php b/application/controllers/ScheduledDowntimeController.php new file mode 100644 index 0000000..e681a70 --- /dev/null +++ b/application/controllers/ScheduledDowntimeController.php @@ -0,0 +1,45 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaScheduledDowntimeRangeForm; +use Icinga\Module\Director\Objects\IcingaScheduledDowntime; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Table\IcingaScheduledDowntimeRangeTable; + +class ScheduledDowntimeController extends ObjectController +{ + protected $objectBaseUrl = 'director/scheduled-downtime'; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/scheduled-downtimes'); + } + + public function rangesAction() + { + /** @var IcingaScheduledDowntime $object */ + $object = $this->object; + $this->tabs()->activate('ranges'); + $this->addTitle($this->translate('Time period ranges')); + $form = IcingaScheduledDowntimeRangeForm::load() + ->setScheduledDowntime($object); + + if (null !== ($name = $this->params->get('range'))) { + $this->addBackLink($this->url()->without('range')); + $form->loadObject([ + 'scheduled_downtime_id' => $object->get('id'), + 'range_key' => $name, + 'range_type' => $this->params->get('range_type') + ]); + } + + $this->content()->add($form->handleRequest()); + IcingaScheduledDowntimeRangeTable::load($object)->renderTo($this); + } + + public function getType() + { + return 'scheduledDowntime'; + } +} diff --git a/application/controllers/ScheduledDowntimesController.php b/application/controllers/ScheduledDowntimesController.php new file mode 100644 index 0000000..b6d314c --- /dev/null +++ b/application/controllers/ScheduledDowntimesController.php @@ -0,0 +1,47 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ScheduledDowntimesController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + $this->tabs()->remove('templates'); + return $res; + } + + protected function getTable() + { + return parent::getTable() + ->setBaseObjectUrl('director/scheduled-downtime'); + } + + protected function getApplyRulesTable() + { + return parent::getApplyRulesTable()->createLinksWithNames(); + } + + public function getType() + { + return 'scheduledDowntime'; + } + + public function getBaseObjectUrl() + { + return 'scheduled-downtime'; + } + + protected function assertApplyRulePermission() + { + return $this->assertPermission('director/scheduled-downtimes'); + } + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/scheduled-downtimes'); + } +} diff --git a/application/controllers/SchemaController.php b/application/controllers/SchemaController.php new file mode 100644 index 0000000..b0ca24e --- /dev/null +++ b/application/controllers/SchemaController.php @@ -0,0 +1,113 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; + +class SchemaController extends ActionController +{ + protected $schemas; + + public function init() + { + $this->schemas = [ + 'mysql' => $this->translate('MySQL schema'), + 'pgsql' => $this->translate('PostgreSQL schema'), + ]; + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + public function mysqlAction() + { + $this->serveSchema('mysql'); + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + public function pgsqlAction() + { + $this->serveSchema('pgsql'); + } + + /** + * @param $type + * @throws \Icinga\Exception\IcingaException + */ + protected function serveSchema($type) + { + $schema = $this->loadSchema($type); + + if ($this->params->get('format') === 'sql') { + header('Content-type: application/octet-stream'); + header('Content-Disposition: attachment; filename=' . $type . '.sql'); + echo $schema; + exit; + // TODO: Shutdown + } + + $this + ->addSchemaTabs($type) + ->addTitle($this->schemas[$type]) + ->addDownloadAction() + ->content()->add(Html::tag('pre', null, $schema)); + } + + protected function loadSchema($type) + { + return file_get_contents( + sprintf( + '%s/schema/%s.sql', + $this->Module()->getBasedir(), + $type + ) + ); + } + + /** + * @return $this + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function addDownloadAction() + { + $this->actions()->add( + Link::create( + $this->translate('Download'), + $this->url()->with('format', 'sql'), + null, + [ + 'target' => '_blank', + 'class' => 'icon-download', + ] + ) + ); + + return $this; + } + + /** + * @param $active + * @return $this + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function addSchemaTabs($active) + { + $tabs = $this->tabs(); + foreach ($this->schemas as $type => $title) { + $tabs->add($type, [ + 'url' => 'director/schema/' . $type, + 'label' => $title, + ]); + } + + $tabs->activate($active); + + return $this; + } +} diff --git a/application/controllers/SelfServiceController.php b/application/controllers/SelfServiceController.php new file mode 100644 index 0000000..0b3b642 --- /dev/null +++ b/application/controllers/SelfServiceController.php @@ -0,0 +1,435 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Forms\IcingaHostSelfServiceForm; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaZone; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Util; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; + +class SelfServiceController extends ActionController +{ + /** @var bool */ + protected $isApified = true; + + /** @var bool */ + protected $requiresAuthentication = false; + + /** @var Settings */ + protected $settings; + + protected function assertApiPermission() + { + // no permission required, we'll check the API key + } + + protected function checkDirectorPermissions() + { + } + + /** + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Zend_Controller_Request_Exception + */ + public function apiVersionAction() + { + if ($this->getRequest()->isApiRequest()) { + $this->sendPowerShellResponse('1.4.0'); + } else { + throw new NotFoundError('Not found'); + } + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Zend_Controller_Exception + */ + public function registerHostAction() + { + $request = $this->getRequest(); + $form = IcingaHostSelfServiceForm::create($this->db()); + $form->setApiRequest($request->isApiRequest()); + try { + if ($key = $this->params->get('key')) { + $form->loadTemplateWithApiKey($key); + } + } catch (Exception $e) { + $this->sendPowerShellError($e->getMessage(), 404); + return; + } + if ($name = $this->params->get('name')) { + $form->setHostName($name); + } + + if ($request->isApiRequest()) { + $data = json_decode($request->getRawBody()); + $request->setPost((array) $data); + $form->handleRequest(); + if ($newKey = $form->getHostApiKey()) { + $this->sendPowerShellResponse($newKey); + } else { + $error = implode('; ', $form->getErrorMessages()); + if ($error === '') { + if ($form->isMissingRequiredFields()) { + $fields = $form->listMissingRequiredFields(); + if (count($fields) === 1) { + $this->sendPowerShellError( + sprintf("%s is required", $fields[0]), + 400 + ); + } else { + $this->sendPowerShellError( + sprintf("Missing parameters: %s", implode(', ', $fields)), + 400 + ); + } + return; + } else { + $this->sendPowerShellError('An unknown error ocurred', 500); + } + } else { + $this->sendPowerShellError($error, 400); + } + } + return; + } + + $form->handleRequest(); + $this->addSingleTab($this->translate('Self Service')) + ->addTitle($this->translate('Self Service - Host Registration')) + ->content()->add(Html::tag('p', null, $this->translate( + 'In case an Icinga Admin provided you with a self service API' + . ' token, this is where you can register new hosts' + ))) + ->add($form); + } + + /** + * @throws NotFoundError + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + public function ticketAction() + { + if (!$this->getRequest()->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + try { + $key = $this->params->getRequired('key'); + $host = IcingaHost::loadWithApiKey($key, $this->db()); + if ($host->isTemplate()) { + throw new NotFoundError('Got invalid API key "%s"', $key); + } + $name = $host->getObjectName(); + + if ($host->getResolvedProperty('has_agent') !== 'y') { + throw new NotFoundError('The host "%s" is not an agent', $name); + } + + $this->sendPowerShellResponse($this->api()->getTicket($name)); + } catch (Exception $e) { + if ($e instanceof NotFoundError) { + $this->sendPowerShellError($e->getMessage(), 404); + } else { + $this->sendPowerShellError($e->getMessage(), 500); + } + } + } + + /** + * @param $response + * @throws ProgrammingError + * @throws \Zend_Controller_Request_Exception + */ + protected function sendPowerShellResponse($response) + { + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + if (is_array($response)) { + echo $this->makePlainTextPowerShellArray($response); + } else { + echo $response; + } + } else { + $this->sendJson($this->getResponse(), $response); + } + } + + /** + * @param $error + * @param $code + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function sendPowerShellError($error, $code) + { + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + $this->getResponse()->setHttpResponseCode($code); + echo "ERROR: $error"; + } else { + $this->sendJsonError($this->getResponse(), $error, $code); + } + } + + /** + * @param $value + * @return string + * @throws ProgrammingError + */ + protected function makePowerShellBoolean($value) + { + if ($value === 'y' || $value === true) { + return 'true'; + } elseif ($value === 'n' || $value === false) { + return 'false'; + } else { + throw new ProgrammingError( + 'Expected boolean value, got %s', + var_export($value, 1) + ); + } + } + + /** + * @param array $params + * @return string + * @throws ProgrammingError + */ + protected function makePlainTextPowerShellArray(array $params) + { + $plain = ''; + + foreach ($params as $key => $value) { + if (is_bool($value)) { + $value = $this->makePowerShellBoolean($value); + } elseif (is_array($value)) { + $value = implode('!', $value); + } + $plain .= "$key: $value\r\n"; + } + + return $plain; + } + + /** + * @throws NotFoundError + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + public function powershellParametersAction() + { + if (!$this->getRequest()->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + try { + $this->shipPowershellParams(); + } catch (Exception $e) { + if ($e instanceof NotFoundError) { + $this->sendPowerShellError($e->getMessage(), 404); + } else { + $this->sendPowerShellError($e->getMessage(), 500); + } + } + } + + /** + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\MissingParameterException + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function shipPowershellParams() + { + $db = $this->db(); + $key = $this->params->getRequired('key'); + $host = IcingaHost::loadWithApiKey($key, $db); + + $settings = $this->getSettings(); + $transform = $settings->get('self-service/transform_hostname'); + $params = [ + 'fetch_agent_name' => $settings->get('self-service/agent_name') === 'hostname', + 'fetch_agent_fqdn' => $settings->get('self-service/agent_name') === 'fqdn', + 'transform_hostname' => $transform, + 'flush_api_directory' => $settings->get('self-service/flush_api_dir') === 'y', + // ConvertEndpointIPConfig: + 'resolve_parent_host' => $settings->get('self-service/resolve_parent_host'), + // InstallFrameworkService: + 'install_framework_service' => '0', + // ServiceDirectory => framework_service_directory + // FrameworkServiceUrl => framework_service_url + // InstallFrameworkPlugins: + 'install_framework_plugins' => '0', + // PluginsUrl => framework_plugins_url + ]; + $username = $settings->get('self-service/icinga_service_user'); + if ($username !== null && strlen($username) > 0) { + $params['icinga_service_user'] = $username; + } + + if ($transform === '2') { + $transformMethod = '.upperCase'; + } elseif ($transform === '1') { + $transformMethod = '.lowerCase'; + } else { + $transformMethod = ''; + } + + $hostObject = (object) [ + 'address' => '&ipaddress&', + ]; + + switch ($settings->get('self-service/agent_name')) { + case 'hostname': + $hostObject->display_name = "&fqdn$transformMethod&"; + break; + case 'fqdn': + $hostObject->display_name = "&hostname$transformMethod&"; + break; + } + $params['director_host_object'] = json_encode($hostObject); + + if ($settings->get('self-service/download_type')) { + $params['download_url'] = $settings->get('self-service/download_url'); + $params['agent_version'] = $settings->get('self-service/agent_version'); + $params['allow_updates'] = $settings->get('self-service/allow_updates') === 'y'; + $params['agent_listen_port'] = $host->getAgentListenPort(); + if ($hashes = $settings->get('self-service/installer_hashes')) { + $params['installer_hashes'] = $hashes; + } + + if ($settings->get('self-service/install_nsclient') === 'y') { + $params['install_nsclient'] = true; + $this->addBooleanSettingsToParams($settings, [ + 'nsclient_add_defaults', + 'nsclient_firewall', + 'nsclient_service', + ], $params); + + + $this->addStringSettingsToParams($settings, [ + 'nsclient_directory', + 'nsclient_installer_path' + ], $params); + } + } + + $this->addHostToParams($host, $params); + + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + echo $this->makePlainTextPowerShellArray($params); + } else { + $this->sendJson($this->getResponse(), $params); + } + } + + /** + * @param IcingaHost $host + * @param array $params + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function addHostToParams(IcingaHost $host, array &$params) + { + if (! $host->isObject()) { + return; + } + + $db = $this->db(); + $settings = $this->getSettings(); + $name = $host->getObjectName(); + if ($host->getSingleResolvedProperty('has_agent') !== 'y') { + $this->sendPowerShellError(sprintf( + '%s is not configured for Icinga Agent usage', + $name + ), 403); + return; + } + + $zoneName = $host->getRenderingZone(); + if ($zoneName === IcingaHost::RESOLVE_ERROR) { + $this->sendPowerShellError(sprintf( + 'Could not resolve target Zone for %s', + $name + ), 404); + return; + } + + $masterConnectsToAgent = $host->getSingleResolvedProperty( + 'master_should_connect' + ) === 'y'; + $params['agent_add_firewall_rule'] = $masterConnectsToAgent; + + $params['global_zones'] = $settings->get('self-service/global_zones'); + + $zone = IcingaZone::load($zoneName, $db); + $endpointNames = $zone->listEndpoints(); + if (! $masterConnectsToAgent) { + $endpointsConfig = []; + foreach ($endpointNames as $endpointName) { + $endpoint = IcingaEndpoint::load($endpointName, $db); + $endpointsConfig[] = sprintf( + '%s;%s', + $endpoint->getSingleResolvedProperty('host'), + $endpoint->getResolvedPort() + ); + } + + $params['endpoints_config'] = $endpointsConfig; + } + $master = $db->getDeploymentEndpoint(); + $params['parent_zone'] = $zoneName; + $params['ca_server'] = $master->getObjectName(); + $params['parent_endpoints'] = $endpointNames; + $params['accept_config'] = $host->getSingleResolvedProperty('accept_config')=== 'y'; + } + + protected function addStringSettingsToParams(Settings $settings, array $keys, array &$params) + { + foreach ($keys as $key) { + $value = $settings->get("self-service/$key"); + if (strlen($value)) { + $params[$key] = $value; + } + } + } + + protected function addBooleanSettingsToParams(Settings $settings, array $keys, array &$params) + { + foreach ($keys as $key) { + $value = $settings->get("self-service/$key"); + if ($value !== null) { + $params[$key] = $value === 'y'; + } + } + } + + /** + * @return Settings + * @throws \Icinga\Exception\ConfigurationError + */ + protected function getSettings() + { + if ($this->settings === null) { + $this->settings = new Settings($this->db()); + } + + return $this->settings; + } +} diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php new file mode 100644 index 0000000..3cd54d6 --- /dev/null +++ b/application/controllers/ServiceController.php @@ -0,0 +1,311 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\UuidLookup; +use Icinga\Module\Director\Forms\IcingaServiceForm; +use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\Table\IcingaAppliedServiceTable; +use Icinga\Web\Widget\Tab; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Widget\Tabs; + +class ServiceController extends ObjectController +{ + /** @var IcingaHost */ + protected $host; + + protected $set; + + protected $apply; + + protected function checkDirectorPermissions() + { + if ($this->hasPermission('director/monitoring/services')) { + $monitoring = new Monitoring(); + if ($monitoring->authCanEditService($this->Auth(), $this->getParam('host'), $this->getParam('name'))) { + return; + } + } + $this->assertPermission('director/hosts'); + } + + public function init() + { + // This happens in parent::init() too, but is required to take place before the next two lines + $this->enableStaticObjectLoader($this->getTableName()); + + // Hint: having Host and Set loaded first is important for UUID lookups with legacy URLs + $this->host = $this->getOptionalRelatedObjectFromParams('host', 'host'); + $this->set = $this->getOptionalRelatedObjectFromParams('service_set', 'set'); + parent::init(); + if ($this->object) { + if ($this->host === null) { + $this->host = $this->loadOptionalRelatedObject($this->object, 'host'); + } + if ($this->set === null) { + $this->set = $this->loadOptionalRelatedObject($this->object, 'service_set'); + } + } + $this->addOptionalHostTabs(); + $this->addOptionalSetTabs(); + } + + protected function getOptionalRelatedObjectFromParams($type, $parameter) + { + if ($id = $this->params->get("${parameter}_id")) { + $key = (int) $id; + } else { + $key = $this->params->get($parameter); + } + if ($key !== null) { + $table = DbObjectTypeRegistry::tableNameByType($type); + $key = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch()); + return $this->loadSpecificObject($table, $key); + } + + return null; + } + + protected function loadOptionalRelatedObject(IcingaObject $object, $relation) + { + $key = $object->getUnresolvedRelated($relation); + if ($key === null) { + if ($key = $object->get("${relation}_id")) { + $key = (int) $key; + } else { + $key = $object->get($relation); + // We reach this when accessing Service Template Fields + } + } + + if ($key === null) { + return null; + } + + $table = DbObjectTypeRegistry::tableNameByType($relation); + $uuid = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch()); + return $this->loadSpecificObject($table, $uuid); + } + + protected function addParamToTabs($name, $value) + { + foreach ($this->tabs()->getTabs() as $tab) { + /** @var Tab $tab */ + $tab->getUrl()->setParam($name, $value); + } + + return $this; + } + + public function addAction() + { + parent::addAction(); + if ($this->host) { + // TODO: use setTitle. And figure out, where we use this old route. + $this->view->title = $this->host->object_name . ': ' . $this->view->title; + } elseif ($this->set) { + $this->view->title = sprintf( + $this->translate('Add a service to "%s"'), + $this->set->object_name + ); + } elseif ($this->apply) { + $this->view->title = sprintf( + $this->translate('Apply "%s"'), + $this->apply->object_name + ); + } + } + + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if ($this->set) { + /** @var IcingaServiceForm$form */ + $form->setServiceSet($this->set); + } + if ($this->object === null && $this->apply) { + $form->createApplyRuleFor($this->apply); + } + } + + public function editAction() + { + $this->tabs()->activate('modify'); + + /** @var IcingaService $object */ + $object = $this->object; + $this->addTitle($object->getObjectName()); + if ($object->isTemplate() && $this->showNotInBranch($this->translate('Modifying Templates'))) { + return; + } + + $form = IcingaServiceForm::load()->setDb($this->db()); + $form->setBranch($this->getBranch()); + + if ($this->host) { + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/host/services', + ['uuid' => $this->host->getUniqueId()->toString()], + ['class' => 'icon-left-big'] + )); + $form->setHost($this->host); + } + + if ($this->set) { + $form->setServiceSet($this->set); + } + if ($this->host && $object->usesVarOverrides()) { + $fake = IcingaService::create(array( + 'object_type' => 'object', + 'host_id' => $object->get('host_id'), + 'imports' => $object, + 'object_name' => $object->object_name, + 'use_var_overrides' => 'y', + 'vars' => $this->host->getOverriddenServiceVars($object->object_name), + ), $this->db()); + + $form->setObject($fake); + } else { + $form->setObject($object); + } + + $form->handleRequest(); + $this->addActionClone(); + + if ($this->host) { + $this->view->subtitle = sprintf( + $this->translate('(on %s)'), + $this->host->object_name + ); + } + + try { + if ($object->isTemplate() + && $object->getResolvedProperty('check_command_id') + ) { + $this->view->actionLinks .= ' ' . $this->view->qlink( + 'Create apply-rule', + 'director/service/add', + array('apply' => $object->object_name), + array('class' => 'icon-plus') + ); + } + } catch (Exception $e) { + // ignore the error, show no apply link + } + + $this->content()->add($form); + } + + public function assignAction() + { + // TODO: figure out whether and where we link to this + /** @var IcingaService $service */ + $service = $this->object; + $this->actions()->add(new Link( + $this->translate('back'), + $this->getRequest()->getUrl()->without('rule_id'), + null, + array('class' => 'icon-left-big') + )); + + $this->tabs()->activate('applied'); + $this->addTitle( + $this->translate('Apply: %s'), + $service->getObjectName() + ); + $table = (new IcingaAppliedServiceTable($this->db())) + ->setService($service); + $table->getAttributes()->set('data-base-target', '_self'); + + $this->content()->add($table); + } + + protected function getLegacyKey() + { + if ($key = $this->params->get('id')) { + $key = (int) $key; + } else { + $key = $this->params->get('name'); + } + + if ($key === null) { + throw new \InvalidArgumentException('uuid, name or id required'); + } + + return $key; + } + + protected function loadObject() + { + if ($this->params->has('uuid')) { + parent::loadObject(); + return; + } + + $key = $this->getLegacyKey(); + // Hint: not passing 'object' as type, we still have name-based links in previews and similar + $uuid = UuidLookup::findServiceUuid($this->db(), $this->getBranch(), null, $key, $this->host, $this->set); + if ($uuid === null) { + if (! $this->params->get('allowOverrides')) { + throw new NotFoundError('Not found'); + } + } else { + $this->params->set('uuid', $uuid->toString()); + parent::loadObject(); + } + } + + protected function addOptionalHostTabs() + { + if ($this->host === null) { + return; + } + $hostname = $this->host->getObjectName(); + $tabs = new Tabs(); + $urlParams = ['uuid' => $this->host->getUniqueId()->toString()]; + $tabs->add('host', [ + 'url' => 'director/host', + 'urlParams' => $urlParams, + 'label' => $this->translate('Host'), + ])->add('services', [ + 'url' => 'director/host/services', + 'urlParams' => $urlParams, + 'label' => $this->translate('Services'), + ]); + + $this->addParamToTabs('host', $hostname); + $this->controls()->prependTabs($tabs); + } + + protected function addOptionalSetTabs() + { + if ($this->set === null) { + return; + } + $setName = $this->set->getObjectName(); + $tabs = new Tabs(); + $tabs->add('set', [ + 'url' => 'director/serviceset', + 'urlParams' => ['name' => $setName], + 'label' => $this->translate('ServiceSet'), + ])->add('services', [ + 'url' => 'director/serviceset/services', + 'urlParams' => ['name' => $setName], + 'label' => $this->translate('Services'), + ]); + + $this->addParamToTabs('serviceset', $setName); + $this->controls()->prependTabs($tabs); + } +} diff --git a/application/controllers/ServiceapplyrulesController.php b/application/controllers/ServiceapplyrulesController.php new file mode 100644 index 0000000..c3a7f2b --- /dev/null +++ b/application/controllers/ServiceapplyrulesController.php @@ -0,0 +1,39 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\RestApi\IcingaObjectsHandler; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\ApplyRulesTable; + +class ServiceapplyrulesController extends ActionController +{ + protected $isApified = true; + + public function indexAction() + { + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + $table = ApplyRulesTable::create('service', $this->db()); +/* + $query = $this->db()->getDbAdapter() + ->select() + ->from('icinga_service') + ->where('object_type = ?', 'apply'); + $rules = IcingaService::loadAll($this->db(), $query); +*/ + + $handler = (new IcingaObjectsHandler( + $request, + $this->getResponse(), + $this->db() + ))->setTable($table); + + $handler->dispatch(); + } +} diff --git a/application/controllers/ServicegroupController.php b/application/controllers/ServicegroupController.php new file mode 100644 index 0000000..b2fc50e --- /dev/null +++ b/application/controllers/ServicegroupController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class ServicegroupController extends ObjectController +{ +} diff --git a/application/controllers/ServicegroupsController.php b/application/controllers/ServicegroupsController.php new file mode 100644 index 0000000..d35e638 --- /dev/null +++ b/application/controllers/ServicegroupsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ServicegroupsController extends ObjectsController +{ +} diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php new file mode 100644 index 0000000..8d178c2 --- /dev/null +++ b/application/controllers/ServicesController.php @@ -0,0 +1,42 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ServicesController extends ObjectsController +{ + protected $multiEdit = array( + 'imports', + 'groups', + 'disabled' + ); + + public function edittemplatesAction() + { + parent::editAction(); + + $objects = $this->loadMultiObjectsFromParams(); + $names = []; + /** @var ExportInterface $object */ + foreach ($objects as $object) { + $names[] = $object->getUniqueIdentifier(); + } + + $url = Url::fromPath('director/basket/add', [ + 'type' => 'ServiceTemplate', + ]); + + $url->getParams()->addValues('names', $names); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + $url, + null, + ['class' => 'icon-tag'] + )); + } +} diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php new file mode 100644 index 0000000..684d2fc --- /dev/null +++ b/application/controllers/ServicesetController.php @@ -0,0 +1,141 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Forms\IcingaServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\Table\IcingaHostsMatchingFilterTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetHostTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable; +use gipfl\IcingaWeb2\Link; + +class ServicesetController extends ObjectController +{ + /** @var IcingaHost */ + protected $host; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/servicesets'); + } + + public function init() + { + if (null !== ($host = $this->params->get('host'))) { + $this->host = IcingaHost::load($host, $this->db()); + } + + parent::init(); + if ($this->object) { + $this->addServiceSetTabs(); + } + } + + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if ($this->host) { + /** @var IcingaServiceSetForm $form */ + $form->setHost($this->host); + } + } + + public function addAction() + { + parent::addAction(); + if ($this->host) { + $this->addTitle( + $this->translate('Add a service set to "%s"'), + $this->host->getObjectName() + ); + } + } + + public function servicesAction() + { + /** @var IcingaServiceSet $set */ + $set = $this->object; + $name = $set->getObjectName(); + $this->tabs()->activate('services'); + $this->addTitle( + $this->translate('Services in this set: %s'), + $name + ); + $this->actions()->add(Link::create( + $this->translate('Add service'), + 'director/service/add', + ['set' => $name], + ['class' => 'icon-plus'] + )); + + IcingaServiceSetServiceTable::load($set) + ->setBranch($this->getBranch()) + ->renderTo($this); + } + + public function hostsAction() + { + /** @var IcingaServiceSet $set */ + $set = $this->object; + $this->tabs()->activate('hosts'); + $this->addTitle( + $this->translate('Hosts using this set: %s'), + $set->getObjectName() + ); + + $table = IcingaServiceSetHostTable::load($set); + if ($table->count()) { + $table->renderTo($this); + } + $filter = $set->get('assign_filter'); + if ($filter !== null && \strlen($filter) > 0) { + $this->content()->add( + IcingaHostsMatchingFilterTable::load(Filter::fromQueryString($filter), $this->db()) + ); + } + } + + protected function addServiceSetTabs() + { + $hexUuid = $this->object->getUniqueId()->toString(); + $tabs = $this->tabs(); + $tabs->add('services', [ + 'url' => 'director/serviceset/services', + 'urlParams' => ['uuid' => $hexUuid], + 'label' => 'Services' + ]); + if ($this->branch->isBranch()) { + return $this; + } + $tabs->add('hosts', [ + 'url' => 'director/serviceset/hosts', + 'urlParams' => ['uuid' => $hexUuid], + 'label' => 'Hosts' + ]); + + return $this; + } + + protected function loadObject() + { + if ($this->object === null) { + if (null !== ($name = $this->params->get('name'))) { + $params = ['object_name' => $name]; + $db = $this->db(); + + if ($this->host) { + $params['host_id'] = $this->host->get('id'); + } + + $this->object = IcingaServiceSet::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } +} diff --git a/application/controllers/ServicetemplateController.php b/application/controllers/ServicetemplateController.php new file mode 100644 index 0000000..25d0742 --- /dev/null +++ b/application/controllers/ServicetemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class ServicetemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaService::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/SettingsController.php b/application/controllers/SettingsController.php new file mode 100644 index 0000000..c4709e6 --- /dev/null +++ b/application/controllers/SettingsController.php @@ -0,0 +1,48 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\KickstartForm; +use Icinga\Module\Director\Forms\SelfServiceSettingsForm; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; + +class SettingsController extends ActionController +{ + /** + * @throws \Icinga\Exception\Http\HttpNotFoundException + */ + public function indexAction() + { + // Hint: this is for the module configuration tab, legacy code + $this->view->tabs = $this->Module() + ->getConfigTabs() + ->activate('config'); + + $this->view->form = KickstartForm::load() + ->setModuleConfig($this->Config()) + ->handleRequest(); + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + */ + public function selfServiceAction() + { + $form = SelfServiceSettingsForm::create($this->db(), new Settings($this->db())); + $form->handleRequest(); + + $hint = $this->translate( + 'The Icinga Director Self Service API allows your Hosts to register' + . ' themselves. This allows them to get their Icinga Agent configured,' + . ' installed and upgraded in an automated way.' + ); + + $this->addSingleTab($this->translate('Self Service')) + ->addTitle($this->translate('Self Service API - Global Settings')) + ->content()->add(Html::tag('p', null, $hint)) + ->add($form); + } +} diff --git a/application/controllers/SuggestController.php b/application/controllers/SuggestController.php new file mode 100644 index 0000000..659c48c --- /dev/null +++ b/application/controllers/SuggestController.php @@ -0,0 +1,415 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use ipl\Html\Html; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Objects\HostApplyMatches; + +class SuggestController extends ActionController +{ + protected function checkDirectorPermissions() + { + } + + public function indexAction() + { + // TODO: Using some temporarily hardcoded methods, should use DataViews later on + $context = $this->getRequest()->getPost('context'); + $key = null; + + if (strpos($context, '!') !== false) { + list($context, $key) = preg_split('~!~', $context, 2); + } + + $func = 'suggest' . ucfirst($context); + if (method_exists($this, $func)) { + if (! empty($key)) { + $all = $this->$func($key); + } else { + $all = $this->$func(); + } + } else { + $all = array(); + } + // TODO: also get cursor position and eventually add an asterisk in the middle + // tODO: filter also when fetching, eventually limit somehow + $search = $this->getRequest()->getPost('value'); + $begins = array(); + $matches = array(); + $begin = Filter::expression('value', '=', $search . '*'); + $middle = Filter::expression('value', '=', '*' . $search . '*')->setCaseSensitive(false); + $prefixes = array(); + foreach ($all as $str) { + if (false !== ($pos = strrpos($str, '.'))) { + $prefix = substr($str, 0, $pos) . '.'; + $prefixes[$prefix] = $prefix; + } + if (strlen($search)) { + $row = (object) array('value' => $str); + if ($begin->matches($row)) { + $begins[] = $this->highlight($str, $search); + } elseif ($middle->matches($row)) { + $matches[] = $this->highlight($str, $search); + } + } else { + $matches[] = Html::escape($str); + } + } + + $containing = array_slice(array_merge($begins, $matches), 0, 100); + $suggestions = $containing; + + if ($func === 'suggestHostFilterColumns' || $func === 'suggestHostaddresses') { + ksort($prefixes); + + if (count($suggestions) < 5) { + $suggestions = array_merge($suggestions, array_keys($prefixes)); + } + } + $this->view->suggestions = $suggestions; + } + + /** + * One more dummy helper for tests + * + * TODO: Should not remain here + * + * @return array + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Security\SecurityException + */ + protected function suggestLocations() + { + $this->assertPermission('director/hosts'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->distinct() + ->from('icinga_host_var', 'varvalue') + ->where('varname = ?', 'location') + ->order('varvalue'); + return $db->fetchCol($query); + } + + protected function suggestHostnames($type = 'object') + { + $this->assertPermission('director/hosts'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_host', 'object_name') + ->order('object_name'); + + if ($type !== null) { + $query->where('object_type = ?', $type); + } + $restriction = new HostgroupRestriction($this->db(), $this->Auth()); + $restriction->filterHostsQuery($query); + + return $db->fetchCol($query); + } + + protected function suggestHostsAndTemplates() + { + return $this->suggestHostnames(null); + } + + protected function suggestServicenames() + { + $r=array(); + $this->assertPermission('director/services'); + $db = $this->db()->getDbAdapter(); + $for_host = $this->getRequest()->getPost('for_host'); + if (!empty($for_host)) { + $tmp_host = IcingaHost::load($for_host, $this->db()); + } + + $query = $db->select()->distinct() + ->from('icinga_service', 'object_name') + ->order('object_name') + ->where("object_type IN ('object','apply')"); + if (!empty($tmp_host)) { + $query->where('host_id = ?', $tmp_host->id); + } + $r = array_merge($r, $db->fetchCol($query)); + if (!empty($tmp_host)) { + $resolver = $tmp_host->templateResolver(); + foreach ($resolver->fetchResolvedParents() as $template_obj) { + $query = $db->select()->distinct() + ->from('icinga_service', 'object_name') + ->order('object_name') + ->where("object_type IN ('object','apply')") + ->where('host_id = ?', $template_obj->id); + $r = array_merge($r, $db->fetchCol($query)); + } + + $matcher = HostApplyMatches::prepare($tmp_host); + foreach ($this->getAllApplyRules() as $rule) { + if ($matcher->matchesFilter($rule->filter)) { //TODO + $r[]=$rule->name; + } + } + } + natcasesort($r); + return $r; + } + + protected function suggestHosttemplates() + { + $this->assertPermission('director/hosts'); + return $this->fetchTemplateNames('icinga_host', 'template_choice_id IS NULL'); + } + + protected function suggestServicetemplates() + { + $this->assertPermission('director/services'); + return $this->fetchTemplateNames('icinga_service', 'template_choice_id IS NULL'); + } + + protected function suggestNotificationtemplates() + { + $this->assertPermission('director/notifications'); + return $this->fetchTemplateNames('icinga_notification'); + } + + protected function suggestCommandtemplates() + { + $this->assertPermission('director/commands'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_command', 'object_name') + ->order('object_name'); + return $db->fetchCol($query); + } + + protected function suggestUsertemplates() + { + $this->assertPermission('director/users'); + return $this->fetchTemplateNames('icinga_user'); + } + + /** + * @return array + * @throws \Icinga\Security\SecurityException + * @codingStandardsIgnoreStart + */ + protected function suggestScheduled_downtimetemplates() + { + // @codingStandardsIgnoreEnd + $this->assertPermission('director/scheduled-downtimes'); + return $this->fetchTemplateNames('icinga_scheduled_downtime'); + } + + protected function suggestCheckcommandnames() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_command', 'object_name') + ->where('object_type != ?', 'template') + ->order('object_name'); + + return $db->fetchCol($query); + } + + protected function fetchTemplateNames($table, $where = null) + { + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from($table, 'object_name') + ->where('object_type = ?', 'template') + ->order('object_name'); + + if ($where !== null) { + $query->where('template_choice_id IS NULL'); + } + + return $db->fetchCol($query); + } + + protected function suggestHostgroupnames() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from('icinga_hostgroup', 'object_name')->order('object_name'); + return $db->fetchCol($query); + } + + protected function suggestHostaddresses() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from('icinga_host', 'address')->order('address'); + return $db->fetchCol($query); + } + + protected function suggestHostFilterColumns() + { + return $this->getFilterColumns('host.', [ + $this->translate('Host properties'), + $this->translate('Custom variables') + ]); + } + + protected function suggestServiceFilterColumns() + { + return $this->getFilterColumns('service.', [ + $this->translate('Service properties'), + $this->translate('Host properties'), + $this->translate('Host Custom variables'), + $this->translate('Custom variables') + ]); + } + + protected function suggestDataListValuesForListId($id) + { + $db = $this->db()->getDbAdapter(); + $select = $db->select() + ->from('director_datalist_entry', ['entry_name', 'entry_value']) + ->where('list_id = ?', $id) + ->order('entry_value ASC'); + + $result = $db->fetchPairs($select); + if ($result) { + return $result; + } else { + return []; + } + } + + protected function suggestDataListValues($field = null) + { + if ($field === null) { + // field is required! + return []; + } + + $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist'; + $db = $this->db()->getDbAdapter(); + + $query = $db->select() + ->from(['f' =>'director_datafield'], []) + ->join( + ['sid' => 'director_datafield_setting'], + 'sid.datafield_id = f.id AND sid.setting_name = \'datalist_id\'', + [] + ) + ->join( + ['l' => 'director_datalist'], + 'l.id = sid.setting_value', + [] + ) + ->join( + ['e' => 'director_datalist_entry'], + 'e.list_id = l.id', + ['entry_name', 'entry_value'] + ) + ->where('datatype = ?', $datalistType) + ->where('varname = ?', $field) + ->order('entry_value'); + + + // TODO: respect allowed_roles + /* this implementation from DataTypeDatalist is broken + $roles = array_map('json_encode', Acl::instance()->listRoleNames()); + + if (empty($roles)) { + $query->where('allowed_roles IS NULL'); + } else { + $query->where('(allowed_roles IS NULL OR allowed_roles IN (?))', $roles); + } + */ + + $data = []; + foreach ($db->fetchPairs($query) as $key => $label) { + // TODO: find a better solution here + // $data[] = sprintf("%s [%s]", $label, $key); + $data[] = $key; + } + return $data; + } + + protected function getFilterColumns($prefix, $keys) + { + if ($prefix === 'host.') { + $all = IcingaHost::enumProperties($this->db(), $prefix); + } else { + $all = IcingaService::enumProperties($this->db(), $prefix); + } + $res = []; + foreach ($keys as $key) { + if (array_key_exists($key, $all)) { + $res = array_merge($res, array_keys($all[$key])); + } + } + + natsort($res); + return $res; + } + + protected function suggestDependencytemplates() + { + $this->assertPermission('director/hosts'); + return $this->fetchTemplateNames('icinga_dependency'); + } + + protected function highlight($val, $search) + { + $search = ($search); + $val = Html::escape($val); + return preg_replace( + '/(' . preg_quote($search, '/') . ')/i', + '<strong>\1</strong>', + $val + ); + } + + protected function getAllApplyRules() + { + $allApplyRules=$this->fetchAllApplyRules(); + foreach ($allApplyRules as $rule) { + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + + return $allApplyRules; + } + + protected function fetchAllApplyRules() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from( + array('s' => 'icinga_service'), + array( + 'id' => 's.id', + 'name' => 's.object_name', + 'assign_filter' => 's.assign_filter', + ) + )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply'); + + return $db->fetchAll($query); + } + + protected function suggestImportsourceproperties($sourceId = null) + { + if ($sourceId === null) { + return []; + } + + try { + $importSource = ImportSource::loadWithAutoIncId($sourceId, $this->db()); + $source = ImportSourceHook::loadByName($importSource->get('source_name'), $this->db()); + + $columns = array_merge( + $source->listColumns(), + $importSource->listProperties() + ); + + return array_combine($columns, $columns); + } catch (NotFoundError $e) { + return []; + } + } +} diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php new file mode 100644 index 0000000..928cf2c --- /dev/null +++ b/application/controllers/SyncruleController.php @@ -0,0 +1,696 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Db\Branch\BranchSupport; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Form\ClickHereForm; +use Icinga\Module\Director\Web\Table\BranchActivityTable; +use Icinga\Module\Director\Web\Widget\IcingaConfigDiff; +use Icinga\Module\Director\Web\Widget\UnorderedList; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Forms\SyncCheckForm; +use Icinga\Module\Director\Forms\SyncPropertyForm; +use Icinga\Module\Director\Forms\SyncRuleForm; +use Icinga\Module\Director\Forms\SyncRunForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Import\Sync; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\SyncRule; +use Icinga\Module\Director\Objects\SyncRun; +use Icinga\Module\Director\Web\Form\CloneSyncRuleForm; +use Icinga\Module\Director\Web\Table\SyncpropertyTable; +use Icinga\Module\Director\Web\Table\SyncRunTable; +use Icinga\Module\Director\Web\Tabs\SyncRuleTabs; +use Icinga\Module\Director\Web\Widget\SyncRunDetails; +use Icinga\Web\Notification; +use ipl\Html\Form; +use ipl\Html\Html; + +class SyncruleController extends ActionController +{ + use BranchHelper; + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->setAutoRefreshInterval(10); + $rule = $this->requireSyncRule(); + $this->tabs(new SyncRuleTabs($rule))->activate('show'); + $ruleName = $rule->get('rule_name'); + $this->addTitle($this->translate('Sync rule: %s'), $ruleName); + + $checkForm = SyncCheckForm::load()->setSyncRule($rule)->handleRequest(); + $store = new DbObjectStore($this->db(), $this->getBranch()); + $runForm = new SyncRunForm($rule, $store); + $runForm->on(SyncRunForm::ON_SUCCESS, function (SyncRunForm $form) { + $message = $form->getSuccessMessage(); + if ($message === null) { + Notification::error($this->translate('Synchronization failed')); + } else { + Notification::success($message); + } + $this->redirectNow($this->url()); + }); + $runForm->handleRequest($this->getServerRequest()); + + if ($lastRunId = $rule->getLastSyncRunId()) { + $run = SyncRun::load($lastRunId, $this->db()); + } else { + $run = null; + } + + $c = $this->content(); + $c->add(Html::tag('p', null, $rule->get('description'))); + if (! $rule->hasSyncProperties()) { + $this->addPropertyHint($rule); + return; + } + $this->addMainActions(); + if (! $run) { + $c->add(Hint::warning($this->translate('This Sync Rule has never been run before.'))); + } + + switch ($rule->get('sync_state')) { + case 'unknown': + $c->add(Html::tag('p', null, $this->translate( + "It's currently unknown whether we are in sync with this rule." + . ' You should either check for changes or trigger a new Sync Run.' + ))); + break; + case 'in-sync': + $c->add(Html::tag('p', null, sprintf( + $this->translate('This Sync Rule was last found to by in Sync at %s.'), + $rule->get('last_attempt') + ))); + /* + TODO: check whether... + - there have been imports since then, differing from former ones + - there have been activities since then + */ + break; + case 'pending-changes': + $c->add(Hint::warning($this->translate( + 'There are pending changes for this Sync Rule. You should trigger a new' + . ' Sync Run.' + ))); + break; + case 'failing': + $c->add(Hint::error(sprintf( + $this->translate( + 'This Sync Rule failed when last checked at %s: %s' + ), + $rule->get('last_attempt'), + $rule->get('last_error_message') + ))); + break; + } + + $c->add($checkForm); + if ($this->hasBranch()) { + $objectType = $rule->get('object_type'); + $table = DbObjectTypeRegistry::tableNameByType($objectType); + if (! BranchSupport::existsForTableName($table)) { + $this->showNotInBranch(sprintf($this->translate("Synchronizing '%s'"), $objectType)); + return; + } + } + + $c->add($runForm); + + if ($run) { + $c->add(Html::tag('h3', null, $this->translate('Last sync run details'))); + $c->add(new SyncRunDetails($run)); + if ($run->get('rule_name') !== $ruleName) { + $c->add(Html::tag('p', null, sprintf( + $this->translate("It has been renamed since then, its former name was %s"), + $run->get('rule_name') + ))); + } + } + } + + /** + * @param SyncRule $rule + */ + protected function addPropertyHint(SyncRule $rule) + { + $this->content()->add(Hint::warning(Html::sprintf( + $this->translate('You must define some %s before you can run this Sync Rule'), + new Link( + $this->translate('Sync Properties'), + 'director/syncrule/property', + ['rule_id' => $rule->get('id')] + ) + ))); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function addAction() + { + $this->editAction(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Exception + */ + public function previewAction() + { + $rule = $this->requireSyncRule(); + $branchSupport = BranchSupport::existsForSyncRule($rule); + $branchStore = new BranchStore($this->db()); + $owner = $this->getAuth()->getUser()->getUsername(); + if ($branchSupport) { + if ($this->getBranch()->isBranch()) { + $tmpBranchName = sprintf( + '%s/%s-%s', + Branch::PREFIX_SYNC_PREVIEW, + $this->getBranch()->getUuid()->toString(), + $rule->get('id') + ); + // We could keep changes for preview on branch too + $branchStore->deleteByName($tmpBranchName); + $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner); + $after = 1600000000; // a date in 2020, minus 10000000 + } else { + $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id'); + $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner); + $after = null; + } + $store = new DbObjectStore($this->db(), $tmpBranch); + } else { + $tmpBranch = $store = null; + } + + $this->tabs(new SyncRuleTabs($rule))->activate('preview'); + $this->addTitle($this->translate('Sync Preview')); + $sync = new Sync($rule, $store); + $keepBranchPreview = false; + if ($tmpBranch) { + if ($lastTime = $branchStore->getLastActivityTime($tmpBranch, $after)) { + if ((time() - $lastTime) > 100) { + $branchStore->wipeBranch($tmpBranch, $after); + } else { + $here = (new ClickHereForm())->handleRequest($this->getServerRequest()); + if ($here->hasBeenClicked()) { + $branchStore->wipeBranch($tmpBranch, $after); + $this->redirectNow($this->url()); + } else { + $keepBranchPreview = true; + } + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('This preview has been generated %s, please click %s to regenerate it'), + DateFormatter::timeAgo($lastTime), + $here + ))); + } + } + } + if (!$keepBranchPreview) { + $modifications = $sync->getExpectedModifications(); + } + + if ($tmpBranch) { + try { + if (!$keepBranchPreview) { + $sync->apply(); + } + } catch (\Exception $e) { + $this->content()->add(Hint::error($e->getMessage())); + return; + } + + $changes = new BranchActivityTable($tmpBranch->getUuid(), $this->db()); + $changes->disableObjectLink(); + if (count($changes) === 0) { + $this->showInSync(); + } + $changes->renderTo($this); + } else { + if (empty($modifications)) { + $this->showInSync(); + return; + } + $this->showExpectedModificationSummary($modifications); + } + } + + protected function showInSync() + { + $this->content()->add(Hint::ok($this->translate( + 'This Sync Rule is in sync and would currently not apply any changes' + ))); + } + + protected function showExpectedModificationSummary($modifications) + { + $create = []; + $modify = []; + $delete = []; + $modifiedProperties = []; + /** @var IcingaObject $object */ + foreach ($modifications as $object) { + if ($object->hasBeenLoadedFromDb()) { + if ($object->shouldBeRemoved()) { + $delete[] = $object; + } else { + $modify[] = $object; + foreach ($object->getModifiedProperties() as $property => $value) { + if (isset($modifiedProperties[$property])) { + $modifiedProperties[$property]++; + } else { + $modifiedProperties[$property] = 1; + } + } + if (! $object instanceof IcingaObject) { + continue; + } + if ($object->supportsGroups()) { + if ($object->hasModifiedGroups()) { + if (isset($modifiedProperties['groups'])) { + $modifiedProperties['groups']++; + } else { + $modifiedProperties['groups'] = 1; + } + } + } + + if ($object->supportsImports()) { + if ($object->imports()->hasBeenModified()) { + if (isset($modifiedProperties['imports'])) { + $modifiedProperties['imports']++; + } else { + $modifiedProperties['imports'] = 1; + } + } + } + if ($object->supportsCustomVars()) { + if ($object->vars()->hasBeenModified()) { + foreach ($object->vars() as $var) { + if ($var->isNew()) { + $varName = 'add vars.' . $var->getKey(); + } elseif ($var->hasBeenDeleted()) { + $varName = 'remove vars.' . $var->getKey(); + } elseif ($var->hasBeenModified()) { + $varName = 'vars.' . $var->getKey(); + } else { + continue; + } + if (isset($modifiedProperties[$varName])) { + $modifiedProperties[$varName]++; + } else { + $modifiedProperties[$varName] = 1; + } + } + } + } + } + } else { + $create[] = $object; + } + } + + $content = $this->content(); + if (! empty($delete)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-cancel action-delete'], sprintf( + $this->translate('%d object(s) will be deleted'), + count($delete) + )), + $this->objectList($delete) + ]); + } + if (! empty($modify)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-wrench action-modify'], sprintf( + $this->translate('%d object(s) will be modified'), + count($modify) + )), + $this->listModifiedProperties($modifiedProperties), + $this->objectList($modify), + ]); + } + if (! empty($create)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-plus action-create'], sprintf( + $this->translate('%d object(s) will be created'), + count($create) + )), + $this->objectList($create) + ]); + } + } + + /** + * @param IcingaObject[] $objects + * @return \ipl\Html\HtmlElement + * @throws \Icinga\Exception\NotFoundError + */ + protected function objectList($objects) + { + return Html::tag('p', $this->firstNames($objects)); + } + + /** + * Lots of duplicated code, this whole diff logic should be mouved to a + * dedicated class + * + * @param IcingaObject[] $objects + * @param int $max + * @return string + * @throws \Icinga\Exception\NotFoundError + */ + protected function firstNames($objects, $max = 50) + { + $names = []; + $list = new UnorderedList(); + $list->addAttributes([ + 'style' => 'list-style-type: none; marign: 0; padding: 0', + ]); + $total = count($objects); + $i = 0; + PrefetchCache::forget(); + IcingaHost::clearAllPrefetchCaches(); // why?? + IcingaService::clearAllPrefetchCaches(); + foreach ($objects as $object) { + $i++; + $name = $this->getObjectNameString($object); + if ($object->hasBeenLoadedFromDb()) { + if ($object instanceof IcingaHost) { + $names[$name] = Link::create( + $name, + 'director/host', + ['name' => $name], + ['data-base-target' => '_next'] + ); + $oldObject = IcingaHost::load($object->getObjectName(), $this->db()); + $cfgNew = new IcingaConfig($this->db()); + $cfgOld = new IcingaConfig($this->db()); + $oldObject->renderToConfig($cfgOld); + $object->renderToConfig($cfgNew); + foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) { + $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file); + $names[$name . '___PREVIEW___' . $file] = $diff; + } + } elseif ($object instanceof IcingaService && $object->isObject()) { + $host = $object->getRelated('host'); + + $names[$name] = Link::create( + $name, + 'director/service/edit', + [ + 'name' => $object->getObjectName(), + 'host' => $host->getObjectName() + ], + ['data-base-target' => '_next'] + ); + $oldObject = IcingaService::load([ + 'host_id' => $host->get('id'), + 'object_name' => $object->getObjectName() + ], $this->db()); + + $cfgNew = new IcingaConfig($this->db()); + $cfgOld = new IcingaConfig($this->db()); + $oldObject->renderToConfig($cfgOld); + $object->renderToConfig($cfgNew); + foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) { + $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file); + $names[$name . '___PREVIEW___' . $file] = $diff; + } + } else { + $names[$name] = $name; + } + } else { + $names[$name] = $name; + } + if ($i === $max) { + break; + } + } + ksort($names); + + foreach ($names as $name) { + $list->addItem($name); + } + + if ($total > $max) { + $list->add(sprintf( + $this->translate('...and %d more'), + $total - $max + )); + } + + return $list; + } + + protected function listModifiedProperties($properties) + { + $list = new UnorderedList(); + foreach ($properties as $property => $cnt) { + $list->addItem("${cnt}x $property"); + } + + return $list; + } + + protected function getObjectNameString($object) + { + if ($object instanceof IcingaService) { + if ($object->isObject()) { + return $object->getRelated('host')->getObjectName() + . ': ' . $object->getObjectName(); + } else { + return $object->getObjectName(); + } + } elseif ($object instanceof IcingaHost) { + return $object->getObjectName(); + } elseif ($object instanceof ExportInterface) { + return $object->getUniqueIdentifier(); + } elseif ($object instanceof IcingaObject) { + return $object->getObjectName(); + } else { + /** @var \Icinga\Module\Director\Data\Db\DbObject $object */ + return json_encode($object->getKeyParams()); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $form = SyncRuleForm::load() + ->setListUrl('director/syncrules') + ->setDb($this->db()); + + if ($id = $this->params->get('id')) { + $form->loadObject((int) $id); + /** @var SyncRule $rule */ + $rule = $form->getObject(); + $this->tabs(new SyncRuleTabs($rule))->activate('edit'); + $this->addTitle(sprintf( + $this->translate('Sync rule: %s'), + $rule->get('rule_name') + )); + $this->addMainActions(); + + if (! $rule->hasSyncProperties()) { + $this->addPropertyHint($rule); + } + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } + + } else { + $this->addTitle($this->translate('Add sync rule')); + $this->tabs(new SyncRuleTabs())->activate('add'); + if ($this->showNotInBranch($this->translate('Creating Sync Rules'))) { + return; + } + } + + $form->handleRequest(); + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function cloneAction() + { + $id = $this->params->getRequired('id'); + $rule = SyncRule::loadWithAutoIncId((int) $id, $this->db()); + $this->tabs()->add('show', [ + 'url' => 'director/syncrule', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Sync rule'), + ])->add('clone', [ + 'url' => 'director/syncrule/clone', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Clone'), + ])->activate('clone'); + $this->addTitle('Clone: %s', $rule->get('rule_name')); + $this->actions()->add( + Link::create( + $this-> |