├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config ├── config.inc.php.dist └── pxe-boot-template.txt.dist ├── debian ├── changelog ├── compat ├── control ├── dirs ├── examples ├── install ├── rules └── source │ ├── format │ └── options ├── examples └── apache-example-vhost.conf ├── includes └── ganetiClient.php ├── public ├── .htaccess ├── assets │ ├── bootstrap │ ├── css │ │ ├── bootstrap-slider.min.css │ │ ├── dashboard.css │ │ └── ganeti-status.css │ ├── datatables │ ├── img │ │ └── ajax-loader.gif │ ├── jquery │ └── js │ │ ├── bootstrap-slider.js │ │ ├── bootstrap-slider.min.js │ │ ├── bootstrap3-typeahead.js │ │ ├── gcc.js │ │ ├── ie10-viewport-bug-workaround.js │ │ ├── jquery.confirm.min.js │ │ └── npm.js └── index.php └── tpl ├── dialogs_cluster.html ├── dialogs_instancedetails.html ├── index.html ├── page_clusternodes.html ├── page_createinstance.html ├── page_instancedetails.html ├── page_instances.html ├── page_jobdetails.html ├── page_jobs.html └── page_statistics.html /.gitignore: -------------------------------------------------------------------------------- 1 | config/config.inc.php 2 | config/pxe-boot-template.txt 3 | vendor 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.0-apache 2 | 3 | ENV APACHE_DOCUMENT_ROOT /var/www/html/public 4 | 5 | RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf 6 | RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf 7 | 8 | RUN a2enmod rewrite 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GANETI CONTROL CENTER 2 | 3 | ## What is this? 4 | 5 | GCC is a frontend to manage ganeti clusters. Essentially, it is a web application based on Slim/Twig, that wraps around a PHP class ```ganetiClient```, which in turn connects to a cluster's RAPI daemon. The class could also be used in other projects/scripts. As of now, GCC does not store any data on its own. However, you need a TFTP server running on the same machine to use the 'preseed feature' (which starts an instance and creates a special tftp/pxe boot configuration for this host). 6 | 7 | ## Who should use this and who should not? 8 | 9 | GCC is tightly build around existing infrastructure, therefore it makes certain assumptions or at least works best in this environment: 10 | * Hypervisor: KVM 11 | * Disk Templates: DRBD 12 | * Instances: independent virtual machines with partition tables, a bootloader etc. (no deboostrap, no integration with ganeti whatsoever) 13 | * Network: the nodes are connected to multiple instance VLANs, each with its own bridge interface namend ```br](http://s15.postimg.org/i0vvsdzq3/gcc_clusternodes.png) 73 | [](http://s8.postimg.org/qm81lh4r9/gcc_instancedetails.png) 74 | [](http://s11.postimg.org/byk0wm65f/gcc_instances.png) 75 | [](http://s32.postimg.org/apwyrcmh1/gcc_stats.png) 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*", 4 | "twig/twig": "v1.*", 5 | "slim/views": "*", 6 | "twitter/bootstrap": "3.*", 7 | "datatables/datatables": "*", 8 | "components/jquery": "*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "c86f4d30549bed06877b84f77d80572f", 8 | "packages": [ 9 | { 10 | "name": "components/jquery", 11 | "version": "3.3.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/components/jquery.git", 15 | "reference": "459648cda77875519c5da3ae1dd0ed5d170aa649" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/components/jquery/zipball/459648cda77875519c5da3ae1dd0ed5d170aa649", 20 | "reference": "459648cda77875519c5da3ae1dd0ed5d170aa649", 21 | "shasum": "" 22 | }, 23 | "type": "component", 24 | "extra": { 25 | "component": { 26 | "scripts": [ 27 | "jquery.js" 28 | ], 29 | "files": [ 30 | "jquery.min.js", 31 | "jquery.min.map", 32 | "jquery.slim.js", 33 | "jquery.slim.min.js", 34 | "jquery.slim.min.map" 35 | ] 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "authors": [ 43 | { 44 | "name": "JS Foundation and other contributors" 45 | } 46 | ], 47 | "description": "jQuery JavaScript Library", 48 | "homepage": "http://jquery.com", 49 | "time": "2018-03-04T13:23:48+00:00" 50 | }, 51 | { 52 | "name": "datatables/datatables", 53 | "version": "1.10.16", 54 | "source": { 55 | "type": "git", 56 | "url": "https://github.com/DataTables/DataTables.git", 57 | "reference": "75a665f64f02982c0f4666b15a25c4670e5e6b18" 58 | }, 59 | "dist": { 60 | "type": "zip", 61 | "url": "https://api.github.com/repos/DataTables/DataTables/zipball/75a665f64f02982c0f4666b15a25c4670e5e6b18", 62 | "reference": "75a665f64f02982c0f4666b15a25c4670e5e6b18", 63 | "shasum": "" 64 | }, 65 | "type": "library", 66 | "notification-url": "https://packagist.org/downloads/", 67 | "license": [ 68 | "MIT" 69 | ], 70 | "description": "DataTables is a plug-in for the jQuery Javascript library. It is a highly flexible tool, based upon the foundations of progressive enhancement, which will add advanced interaction controls to any HTML table.", 71 | "homepage": "http://www.datatables.net/", 72 | "time": "2017-08-31T13:52:17+00:00" 73 | }, 74 | { 75 | "name": "slim/slim", 76 | "version": "2.6.3", 77 | "source": { 78 | "type": "git", 79 | "url": "https://github.com/slimphp/Slim.git", 80 | "reference": "9224ed81ac1c412881e8d762755e3d76ebf580c0" 81 | }, 82 | "dist": { 83 | "type": "zip", 84 | "url": "https://api.github.com/repos/slimphp/Slim/zipball/9224ed81ac1c412881e8d762755e3d76ebf580c0", 85 | "reference": "9224ed81ac1c412881e8d762755e3d76ebf580c0", 86 | "shasum": "" 87 | }, 88 | "require": { 89 | "php": ">=5.3.0" 90 | }, 91 | "suggest": { 92 | "ext-mcrypt": "Required for HTTP cookie encryption", 93 | "phpseclib/mcrypt_compat": "Polyfil for mcrypt extension" 94 | }, 95 | "type": "library", 96 | "autoload": { 97 | "psr-0": { 98 | "Slim": "." 99 | } 100 | }, 101 | "notification-url": "https://packagist.org/downloads/", 102 | "license": [ 103 | "MIT" 104 | ], 105 | "authors": [ 106 | { 107 | "name": "Josh Lockhart", 108 | "email": "info@joshlockhart.com", 109 | "homepage": "http://www.joshlockhart.com/" 110 | } 111 | ], 112 | "description": "Slim Framework, a PHP micro framework", 113 | "homepage": "http://github.com/codeguy/Slim", 114 | "keywords": [ 115 | "microframework", 116 | "rest", 117 | "router" 118 | ], 119 | "time": "2017-01-07T12:21:41+00:00" 120 | }, 121 | { 122 | "name": "slim/views", 123 | "version": "0.1.3", 124 | "source": { 125 | "type": "git", 126 | "url": "https://github.com/slimphp/Slim-Views.git", 127 | "reference": "8561c785e55a39df6cb6f95c3aba3281a60ed5b0" 128 | }, 129 | "dist": { 130 | "type": "zip", 131 | "url": "https://api.github.com/repos/slimphp/Slim-Views/zipball/8561c785e55a39df6cb6f95c3aba3281a60ed5b0", 132 | "reference": "8561c785e55a39df6cb6f95c3aba3281a60ed5b0", 133 | "shasum": "" 134 | }, 135 | "require": { 136 | "php": ">=5.3.0", 137 | "slim/slim": ">=2.4.0" 138 | }, 139 | "suggest": { 140 | "smarty/smarty": "Smarty templating system", 141 | "twig/twig": "Twig templating system" 142 | }, 143 | "type": "library", 144 | "autoload": { 145 | "psr-4": { 146 | "Slim\\Views\\": "./" 147 | } 148 | }, 149 | "notification-url": "https://packagist.org/downloads/", 150 | "license": [ 151 | "MIT" 152 | ], 153 | "authors": [ 154 | { 155 | "name": "Josh Lockhart", 156 | "email": "info@joshlockhart.com", 157 | "homepage": "http://www.joshlockhart.com/" 158 | }, 159 | { 160 | "name": "Andrew Smith", 161 | "email": "a.smith@silentworks.co.uk", 162 | "homepage": "http://thoughts.silentworks.co.uk/" 163 | } 164 | ], 165 | "description": "Smarty and Twig View Parser package for the Slim Framework", 166 | "homepage": "http://github.com/codeguy/Slim-Views", 167 | "keywords": [ 168 | "extensions", 169 | "slimphp", 170 | "templating" 171 | ], 172 | "time": "2014-12-09T23:48:51+00:00" 173 | }, 174 | { 175 | "name": "twig/twig", 176 | "version": "v1.35.2", 177 | "source": { 178 | "type": "git", 179 | "url": "https://github.com/twigphp/Twig.git", 180 | "reference": "9c24f2cd39dc1906b76879e099970b7e53724601" 181 | }, 182 | "dist": { 183 | "type": "zip", 184 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/9c24f2cd39dc1906b76879e099970b7e53724601", 185 | "reference": "9c24f2cd39dc1906b76879e099970b7e53724601", 186 | "shasum": "" 187 | }, 188 | "require": { 189 | "php": ">=5.3.3" 190 | }, 191 | "require-dev": { 192 | "psr/container": "^1.0", 193 | "symfony/debug": "~2.7", 194 | "symfony/phpunit-bridge": "~3.3@dev" 195 | }, 196 | "type": "library", 197 | "extra": { 198 | "branch-alias": { 199 | "dev-master": "1.35-dev" 200 | } 201 | }, 202 | "autoload": { 203 | "psr-0": { 204 | "Twig_": "lib/" 205 | }, 206 | "psr-4": { 207 | "Twig\\": "src/" 208 | } 209 | }, 210 | "notification-url": "https://packagist.org/downloads/", 211 | "license": [ 212 | "BSD-3-Clause" 213 | ], 214 | "authors": [ 215 | { 216 | "name": "Fabien Potencier", 217 | "email": "fabien@symfony.com", 218 | "homepage": "http://fabien.potencier.org", 219 | "role": "Lead Developer" 220 | }, 221 | { 222 | "name": "Armin Ronacher", 223 | "email": "armin.ronacher@active-4.com", 224 | "role": "Project Founder" 225 | }, 226 | { 227 | "name": "Twig Team", 228 | "homepage": "http://twig.sensiolabs.org/contributors", 229 | "role": "Contributors" 230 | } 231 | ], 232 | "description": "Twig, the flexible, fast, and secure template language for PHP", 233 | "homepage": "http://twig.sensiolabs.org", 234 | "keywords": [ 235 | "templating" 236 | ], 237 | "time": "2018-03-03T16:21:29+00:00" 238 | }, 239 | { 240 | "name": "twitter/bootstrap", 241 | "version": "v3.3.7", 242 | "source": { 243 | "type": "git", 244 | "url": "https://github.com/twbs/bootstrap.git", 245 | "reference": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 246 | }, 247 | "dist": { 248 | "type": "zip", 249 | "url": "https://api.github.com/repos/twbs/bootstrap/zipball/0b9c4a4007c44201dce9a6cc1a38407005c26c86", 250 | "reference": "0b9c4a4007c44201dce9a6cc1a38407005c26c86", 251 | "shasum": "" 252 | }, 253 | "replace": { 254 | "twitter/bootstrap": "self.version" 255 | }, 256 | "type": "library", 257 | "extra": { 258 | "branch-alias": { 259 | "dev-master": "3.3.x-dev" 260 | } 261 | }, 262 | "notification-url": "https://packagist.org/downloads/", 263 | "license": [ 264 | "MIT" 265 | ], 266 | "authors": [ 267 | { 268 | "name": "Jacob Thornton", 269 | "email": "jacobthornton@gmail.com" 270 | }, 271 | { 272 | "name": "Mark Otto", 273 | "email": "markdotto@gmail.com" 274 | } 275 | ], 276 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 277 | "homepage": "http://getbootstrap.com", 278 | "keywords": [ 279 | "JS", 280 | "css", 281 | "framework", 282 | "front-end", 283 | "less", 284 | "mobile-first", 285 | "responsive", 286 | "web" 287 | ], 288 | "time": "2016-07-25T15:51:55+00:00" 289 | } 290 | ], 291 | "packages-dev": [], 292 | "aliases": [], 293 | "minimum-stability": "stable", 294 | "stability-flags": [], 295 | "prefer-stable": false, 296 | "prefer-lowest": false, 297 | "platform": [], 298 | "platform-dev": [] 299 | } 300 | -------------------------------------------------------------------------------- /config/config.inc.php.dist: -------------------------------------------------------------------------------- 1 | 50, # How many instances should be displayed per table-page? 23 | ); 24 | 25 | # ganeti RAPI creds 26 | $config["rapi"][] = array( 27 | "name" => "Sample-Cluster", 28 | "host" => "some.server.net", 29 | "user" => "gcc", 30 | "password" => "password", 31 | "spice-ca" => "", # this holds the contents of /var/lib/ganeti/spice-ca.pem 32 | "exclusion-tag" => "a", 33 | "default-os" => "noop", 34 | "auto-install-instance" => false 35 | ); 36 | 37 | # repeat the above block to add multiple clusters 38 | 39 | # configure VLANs for ganeti clusters (array key corresponds to "name" parameter) 40 | $config["vlans"]["Sample-Cluster"] = array( 41 | "Default" => 1, 42 | "Servers" => 2, 43 | "Dev" => 3, 44 | "Setup" => 10, 45 | ); 46 | 47 | ?> 48 | -------------------------------------------------------------------------------- /config/pxe-boot-template.txt.dist: -------------------------------------------------------------------------------- 1 | DEFAULT vesamenu.c32 2 | PROMPT 0 3 | TIMEOUT 10 4 | 5 | MENU TITLE JESSIE BOOT 6 | 7 | LABEL jessie64auto 8 | MENU LABEL ^Jessie64 9 | kernel jessie/amd64/linux 10 | append vga=normal initrd=jessie/amd64/initrd.gz auto preseed/url=http://preseed.host/preseeding.cfg preseed/url/checksum=MD5SUMHERE interface=eth0 locale=en_US keyboard-configuration/xkb-keymap=de hostname=debianpreseed domain=local -- 11 | 12 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | ganeti-control-center (0.1) unstable; urgency=low 2 | 3 | * Initial debian package release 4 | 5 | -- Rudolph Bott Wed, 14 Mar 2018 08:38:12 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: ganeti-control-center 2 | Section: web 3 | Priority: optional 4 | Maintainer: Rudolph Bott 5 | Build-Depends: debhelper (>= 9), pkg-php-tools, composer-php 6 | Standards-Version: 3.9.5 7 | 8 | Package: ganeti-control-center 9 | Architecture: all 10 | Depends: ${misc:Depends}, php5, php5-json, php5-curl 11 | Description: Ganeti Control Center is a PHP based web frontend to the Ganeti REST API (RAPI) 12 | Ganeti Control Center is a PHP based web frontend to the Ganeti REST API (RAPI), allowing you to 13 | create and manage instances as well as migrating them between Ganeti nodes. 14 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | etc/ganeti-control-center 2 | usr/share/ganeti-control-center -------------------------------------------------------------------------------- /debian/examples: -------------------------------------------------------------------------------- 1 | config/config.inc.php.dist 2 | config/pxe-boot-template.txt.dist 3 | examples/* -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | includes usr/share/ganeti-control-center 2 | public usr/share/ganeti-control-center 3 | tpl usr/share/ganeti-control-center 4 | vendor /usr/share/ganeti-control-center 5 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #DH_VERBOSE = 1 5 | 6 | # see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* 7 | DPKG_EXPORT_BUILDFLAGS = 1 8 | include /usr/share/dpkg/default.mk 9 | 10 | # see FEATURE AREAS in dpkg-buildflags(1) 11 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 12 | 13 | # see ENVIRONMENT in dpkg-buildflags(1) 14 | # package maintainers to append CFLAGS 15 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 16 | # package maintainers to append LDFLAGS 17 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 18 | 19 | 20 | # main packaging script based on dh7 syntax 21 | %: 22 | dh $@ 23 | 24 | build-indep: 25 | composer install 26 | 27 | # debmake generated override targets 28 | # This is example for Cmake (See http://bugs.debian.org/641051 ) 29 | #override_dh_auto_configure: 30 | # dh_auto_configure -- \ 31 | # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | compression = "gzip" -------------------------------------------------------------------------------- /examples/apache-example-vhost.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName my.domain.com 3 | Redirect permanent / https://my.domain.com/ 4 | 5 | 6 | 7 | ServerName my.domain.com 8 | ServerAdmin webmaster@my.domain.com 9 | 10 | DocumentRoot /usr/share/ganeti-control-center/public 11 | 12 | 13 | Options FollowSymLinks 14 | AllowOverride None 15 | 16 | 17 | Options Indexes FollowSymLinks MultiViews 18 | 19 | AllowOverride All 20 | 21 | Order allow,deny 22 | Allow From All 23 | 24 | ###### LDAP Auth Example ###### 25 | #AuthBasicProvider ldap 26 | #AuthType basic 27 | #AuthName "ganeti control center" 28 | #AuthLDAPUrl ldap://my.first.ldap.host:389,my.second.ldap.host:389/ou=users,dc=myOrg,dc=com??sub?objectClass=posixAccount TLS 29 | #AuthLDAPGroupAttribute memberUid 30 | #AuthLDAPGroupAttributeIsDN off 31 | #Require ldap-group cn=ganeti-admins,ou=groups,dc=myOrg,dc=com 32 | ###### LDAP Auth Example ###### 33 | 34 | ###### File Auth Example ###### 35 | #AuthType basic 36 | #AuthName "ganeti control center" 37 | #AuthUserFile /etc/ganeti-control-center/web-users 38 | #Require valid-user 39 | 40 | # generate the users file with "htpasswd -c /etc/ganeti-control-center/web-users" and make sure it is not world-readable! 41 | ###### File Auth Example ###### 42 | 43 | 44 | 45 | ErrorLog /var/log/apache2/ganeti-control-center-error.log 46 | LogLevel warn 47 | CustomLog /var/log/apache2/ganeti-control-center-access.log combined 48 | 49 | SSLEngine on 50 | SSLCertificateFile /etc/ssl/certs/my.domain.com.pem 51 | SSLCertificateKeyFile /etc/ssl/private/my.domain.com.pem 52 | # if your certificate requires additional intermediate certificates 53 | #SSLCertificateChainFile /etc/ssl/certs/trust-chain.pem 54 | SSLProtocol -All +TLSv1.2 55 | SSLHonorCipherOrder On 56 | SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS 57 | 58 | -------------------------------------------------------------------------------- /includes/ganetiClient.php: -------------------------------------------------------------------------------- 1 | config = $config; 25 | } 26 | 27 | function callApi($method, $url, $data = false) 28 | { 29 | $curl = curl_init(); 30 | 31 | switch ($method) 32 | { 33 | case "POST": 34 | curl_setopt($curl, CURLOPT_POST, 1); 35 | 36 | if ($data) { 37 | $data_string = json_encode($data); 38 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 39 | 'Content-Type: application/json', 40 | 'Content-Length: ' . strlen($data_string)) 41 | ); 42 | 43 | curl_setopt($curl, CURLOPT_POSTFIELDS, $data_string); 44 | } 45 | break; 46 | case "PUT": 47 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT"); 48 | 49 | if ($data) { 50 | $data_string = json_encode($data); 51 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( 52 | 'Content-Type: application/json', 53 | 'Content-Length: ' . strlen($data_string)) 54 | ); 55 | 56 | curl_setopt($curl, CURLOPT_POSTFIELDS, $data_string); 57 | } 58 | 59 | break; 60 | case "PUT_W_GET_PARAMS": 61 | # this is only here to make the broken "tags" part of RAPI work 62 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT"); 63 | 64 | if ($data) 65 | $url = sprintf("%s?%s", $url, http_build_query($data)); 66 | 67 | break; 68 | default: 69 | if ($data) 70 | $url = sprintf("%s?%s", $url, http_build_query($data)); 71 | } 72 | 73 | // Optional Authentication: 74 | curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); 75 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 76 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); 77 | if(isset($this->config["user"])) { 78 | curl_setopt($curl, CURLOPT_USERPWD, $this->config["user"] . ":" . $this->config["password"]); 79 | } 80 | 81 | curl_setopt($curl, CURLOPT_URL, "https://" . $this->config["host"] . ":5080" . $url); 82 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 83 | 84 | $result = curl_exec($curl); 85 | print_r(curl_error($curl)); 86 | 87 | curl_close($curl); 88 | 89 | return $result; 90 | } 91 | 92 | function getConfigName() { 93 | return $this->config["name"]; 94 | } 95 | 96 | function getClusterInfo() { 97 | $data = json_decode($this->callApi("GET","/2/info"),true); 98 | return $data; 99 | } 100 | 101 | function getNodes($bulk = false) { 102 | if($bulk) { 103 | $data = json_decode($this->callApi("GET","/2/nodes",array("bulk" => "1")),true); 104 | } 105 | else { 106 | $data = json_decode($this->callApi("GET","/2/nodes"),true); 107 | } 108 | return $data; 109 | } 110 | 111 | function getInstances($bulk = false) { 112 | if($bulk) { 113 | $data = json_decode($this->callApi("GET","/2/instances",array("bulk" => "1")),true); 114 | } 115 | else { 116 | $data = json_decode($this->callApi("GET","/2/instances"),true); 117 | } 118 | return $data; 119 | } 120 | 121 | function getInstance($name) { 122 | $data = json_decode($this->callApi("GET","/2/instances/" . $name),true); 123 | return $data; 124 | } 125 | 126 | function getInstanceConsole($name) { 127 | $data = json_decode($this->callApi("GET","/2/instances/" . $name . "/console"),true); 128 | return $data; 129 | } 130 | 131 | function createInstance($params) { 132 | $data = json_decode($this->callApi("POST","/2/instances", $params),true); 133 | return $data; 134 | } 135 | 136 | function createMultiInstance($params) { 137 | $data = json_decode($this->callApi("POST","/2/instances-multi-alloc", $params),true); 138 | return $data; 139 | } 140 | 141 | function startInstance($instance) { 142 | $data = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/startup"),true); 143 | return $data; 144 | } 145 | 146 | function shutdownInstance($instance, $timeout = 120) { 147 | $params = array( 148 | "timeout" => $timeout 149 | ); 150 | $data = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/shutdown", $params),true); 151 | return $data; 152 | } 153 | 154 | function failoverInstance($instance) { 155 | $data = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/failover"),true); 156 | return $data; 157 | } 158 | 159 | function migrateInstance($instance) { 160 | $data = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/migrate"),true); 161 | return $data; 162 | } 163 | 164 | function setInstanceParameter($instance, $type, $parameter, $value) { 165 | switch($type) { 166 | case "beparam": 167 | $data = array( 168 | "beparams" => array( 169 | $parameter => $value), 170 | ); 171 | $return = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify",$data), true); 172 | return $return; 173 | break; 174 | case "hvparam": 175 | $data = array( 176 | "hvparams" => array( 177 | $parameter => $value), 178 | ); 179 | $return = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify", $data), true); 180 | return $return; 181 | break; 182 | } 183 | } 184 | 185 | function setInstanceParameters($instance, $beparams = array(), $hvparams = array()) { 186 | if(!empty($beparams)) $data["beparams"] = $beparams; 187 | if(!empty($hvparams)) $data["hvparams"] = $hvparams; 188 | $return = json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify", $data), true); 189 | return $return; 190 | } 191 | 192 | function setClusterParameter($section, $type, $parameter, $value) { 193 | $return = -1; 194 | switch($section) { 195 | case "hvparams": 196 | $data = array( 197 | "hvparams" => array( 198 | $type => array( 199 | $parameter => $value, 200 | ), 201 | ), 202 | ); 203 | $return = json_decode($this->callApi("PUT", "/2/modify", $data), true); 204 | break; 205 | case "ipolicy": 206 | $data = array( 207 | "ipolicy" => array( 208 | $parameter => $value 209 | ), 210 | ); 211 | $return = json_decode($this->callApi("PUT", "/2/modify", $data), true); 212 | } 213 | return $return; 214 | } 215 | 216 | function setNicParameters($instance, $nic, $mac, $link) { 217 | $data = array( 218 | "nics" => array( 219 | array( 220 | "modify", 221 | $nic, 222 | array( 223 | "link" => $link, 224 | ), 225 | ), 226 | ), 227 | ); 228 | 229 | $origInstance = $this->getInstance($instance); 230 | if($origInstance["nic.macs"][$nic] != $mac) { 231 | $data["nics"][0][2]["mac"] = $mac; 232 | } 233 | 234 | return json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify", $data), true); 235 | } 236 | 237 | function addNic($instance, $mac, $link) { 238 | $data = array( 239 | "nics" => array( 240 | array( 241 | "add", 242 | -1, 243 | array( 244 | "mac" => $mac, 245 | "link" => $link, 246 | ), 247 | ), 248 | ), 249 | ); 250 | 251 | $origInstance = $this->getInstance($instance); 252 | 253 | return json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify", $data), true); 254 | } 255 | 256 | function addDisk($instance, $size) { 257 | $data = array( 258 | "disks" => array( 259 | array( 260 | "add", 261 | -1, 262 | array( 263 | "size" => $size . "g" 264 | ), 265 | ), 266 | ), 267 | ); 268 | 269 | $origInstance = $this->getInstance($instance); 270 | 271 | return json_decode($this->callApi("PUT","/2/instances/" . $instance . "/modify", $data), true); 272 | } 273 | 274 | function addTag($instance, $tag) { 275 | $data["tag"] = $tag; 276 | return json_decode($this->callApi("PUT_W_GET_PARAMS","/2/instances/" . $instance . "/tags", $data), true); 277 | } 278 | 279 | function growDisk($instance, $disk, $amount) { 280 | $data = array( 281 | "amount" => $amount, 282 | "absolute" => false, 283 | "wait_for_sync" => false, 284 | ); 285 | 286 | return json_decode($this->callApi("POST","/2/instances/" . $instance . "/disk/" . $disk . "/grow", $data), true); 287 | } 288 | 289 | function getJobs($bulk = false) { 290 | if($bulk) { 291 | $data = json_decode($this->callApi("GET","/2/jobs",array("bulk" => "1")),true); 292 | for($i = 0; $i < count($data); $i++) { 293 | if(isset($data[$i]["summary"][0]) && preg_match('/^INSTANCE_.+\((.+)\)$/',$data[$i]["summary"][0],$hits)) { 294 | $data[$i]["instanceLink"] = $hits[1]; 295 | } 296 | else { 297 | $data[$i]["instanceLink"] = false; 298 | } 299 | } 300 | } 301 | else { 302 | $data = json_decode($this->callApi("GET","/2/jobs"),true); 303 | } 304 | return $data; 305 | } 306 | 307 | function getJob($jobid) { 308 | $data = json_decode($this->callApi("GET","/2/jobs/" . $jobid),true); 309 | return $data; 310 | } 311 | 312 | function getGroups($bulk = false) { 313 | if($bulk) { 314 | $data = json_decode($this->callApi("GET","/2/groups",array("bulk" => "1")),true); 315 | } 316 | else { 317 | $data = json_decode($this->callApi("GET","/2/groups"),true); 318 | } 319 | return $data; 320 | } 321 | 322 | function getStats() { 323 | $stats["instance-count"] = 0; 324 | $stats["instances"]["memory"] = 0; 325 | $stats["instances"]["disk"] = 0; 326 | $stats["instances"]["cpus"] = 0; 327 | $stats["node-count"] = 0; 328 | $stats["nodes"]["memory"] = 0; 329 | $stats["nodes"]["disk"] = 0; 330 | $stats["nodes"]["cpus"] = 0; 331 | 332 | 333 | # load instances 334 | $instances = $this->getInstances(true); 335 | foreach($instances AS $instance) { 336 | # sum up memory/cpus (cluster-wide) 337 | $stats["instance-count"]++; 338 | $stats["instances"]["memory"] += $instance["beparams"]["memory"]; 339 | $stats["instances"]["cpus"] += $instance["beparams"]["vcpus"]; 340 | 341 | # sum up memory/cpus (per node) 342 | isset($stats["pernode"][$instance["pnode"]]["memory"]) ? $stats["pernode"][$instance["pnode"]]["memory"] += $instance["beparams"]["memory"] : $stats["pernode"][$instance["pnode"]]["memory"] = $instance["beparams"]["memory"]; 343 | isset($stats["pernode"][$instance["pnode"]]["vcpus"]) ? $stats["pernode"][$instance["pnode"]]["vcpus"] += $instance["beparams"]["vcpus"] : $stats["pernode"][$instance["pnode"]]["vcpus"] = $instance["beparams"]["vcpus"]; 344 | 345 | # same for any disk(s) that might exist 346 | foreach($instance["disk.sizes"] AS $disk) { 347 | $stats["instances"]["disk"] += $disk; 348 | isset($stats["pernode"][$instance["pnode"]]["disk"]) ? $stats["pernode"][$instance["pnode"]]["disk"] += $disk : $stats["pernode"][$instance["pnode"]]["disk"] = $disk; 349 | } 350 | 351 | # count instances per node 352 | isset($stats["pnode-counter"][$instance["pnode"]]) ? $stats["pnode-counter"][$instance["pnode"]]++ : $stats["pnode-counter"][$instance["pnode"]] = 1; 353 | } 354 | # calculate instances-per-node-data 355 | $stats["min-inst-per-node"] = min($stats["pnode-counter"]); 356 | $stats["max-inst-per-node"] = max($stats["pnode-counter"]); 357 | $stats["avg-inst-per-node"] = round(array_sum($stats["pnode-counter"]) / count($stats["pnode-counter"])); 358 | unset($stats["pnode-counter"]); 359 | 360 | # load cluster nodes 361 | $nodes = $this->getNodes(true); 362 | foreach($nodes AS $node) { 363 | # sum up memory/cpu/disk totals (cluster-wide) 364 | $stats["node-count"]++; 365 | $stats["nodes"]["memory"] += $node["mtotal"]; 366 | $stats["nodes"]["disk"] += $node["dtotal"]; 367 | $stats["nodes"]["cpus"] += $node["ctotal"]; 368 | 369 | # save per-node data 370 | $stats["pernode"][$node["name"]]["memtotal"] = $node["mtotal"]; 371 | $stats["pernode"][$node["name"]]["disktotal"] = $node["dtotal"]; 372 | $stats["pernode"][$node["name"]]["cpus"] = $node["ctotal"]; 373 | } 374 | 375 | # calculate memory/disk diffs 376 | $stats["nodes"]["memfree"] = $stats["nodes"]["memory"] - $stats["instances"]["memory"]; 377 | $stats["nodes"]["diskfree"] = $stats["nodes"]["disk"] - $stats["instances"]["disk"]; 378 | foreach($stats["pernode"] AS $node => $data) { 379 | @$stats["pernode"][$node]["memfree"] = $data["memtotal"] - $data["memory"]; 380 | @$stats["pernode"][$node]["diskfree"] = $data["disktotal"] - $data["disk"]; 381 | } 382 | 383 | # calculate real-to-virtual-cpu-ratio 384 | $stats["nodes"]["cpu-ratio"] = round($stats["instances"]["cpus"] / $stats["nodes"]["cpus"],2); 385 | 386 | return $stats; 387 | } 388 | 389 | } 390 | ?> 391 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | # RewriteBase / 8 | 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteCond %{REQUEST_FILENAME} !-f 11 | RewriteRule ^ index.php [QSA,L] 12 | -------------------------------------------------------------------------------- /public/assets/bootstrap: -------------------------------------------------------------------------------- 1 | ../../vendor/twitter/bootstrap/dist/ -------------------------------------------------------------------------------- /public/assets/css/bootstrap-slider.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================= 2 | VERSION 6.0.6 3 | ========================================================= */ 4 | /*! ========================================================= 5 | * bootstrap-slider.js 6 | * 7 | * Maintainers: 8 | * Kyle Kemp 9 | * - Twitter: @seiyria 10 | * - Github: seiyria 11 | * Rohit Kalkur 12 | * - Twitter: @Rovolutionary 13 | * - Github: rovolution 14 | * 15 | * ========================================================= 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software 24 | * distributed under the License is distributed on an "AS IS" BASIS, 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | * See the License for the specific language governing permissions and 27 | * limitations under the License. 28 | * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px;margin-top:-5px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;margin-left:-5px;left:50%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-left:-5px;margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;margin-left:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:-o-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0);opacity:1} -------------------------------------------------------------------------------- /public/assets/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | position: fixed; 39 | top: 51px; 40 | bottom: 0; 41 | left: 0; 42 | z-index: 1000; 43 | display: block; 44 | padding: 20px; 45 | overflow-x: hidden; 46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 47 | background-color: #f5f5f5; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | /* Sidebar navigation */ 53 | .nav-sidebar { 54 | margin-right: -21px; /* 20px padding + 1px border */ 55 | margin-bottom: 20px; 56 | margin-left: -20px; 57 | } 58 | .nav-sidebar > li > a { 59 | padding-right: 20px; 60 | padding-left: 20px; 61 | } 62 | .nav-sidebar > .active > a, 63 | .nav-sidebar > .active > a:hover, 64 | .nav-sidebar > .active > a:focus { 65 | color: #fff; 66 | background-color: #428bca; 67 | } 68 | 69 | 70 | /* 71 | * Main content 72 | */ 73 | 74 | .main { 75 | padding: 20px; 76 | } 77 | @media (min-width: 768px) { 78 | .main { 79 | padding-right: 40px; 80 | padding-left: 40px; 81 | } 82 | } 83 | .main .page-header { 84 | margin-top: 0; 85 | } 86 | 87 | 88 | /* 89 | * Placeholder dashboard ideas 90 | */ 91 | 92 | .placeholders { 93 | margin-bottom: 30px; 94 | text-align: center; 95 | } 96 | .placeholders h4 { 97 | margin-bottom: 0; 98 | } 99 | .placeholder { 100 | margin-bottom: 20px; 101 | } 102 | .placeholder img { 103 | display: inline-block; 104 | border-radius: 50%; 105 | } 106 | -------------------------------------------------------------------------------- /public/assets/css/ganeti-status.css: -------------------------------------------------------------------------------- 1 | .job_error { 2 | color: red; 3 | } 4 | 5 | .job_running { 6 | color: blue; 7 | } 8 | 9 | .job_waiting { 10 | color: orange; 11 | } 12 | 13 | .job_success { 14 | color: green; 15 | } 16 | 17 | .job_queued { 18 | color: brown; 19 | } 20 | 21 | .alloc_policy_last_resort { 22 | color: orange; 23 | } 24 | 25 | .alloc_policy_unallocable { 26 | color: red; 27 | } 28 | 29 | .alloc_policy_preferred { 30 | color: green; 31 | } 32 | -------------------------------------------------------------------------------- /public/assets/datatables: -------------------------------------------------------------------------------- 1 | ../../vendor/datatables/datatables/media/ -------------------------------------------------------------------------------- /public/assets/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sipgate/ganeti-control-center-archived/2f8689c527d6ad192c9197076040b2f2589ebe96/public/assets/img/ajax-loader.gif -------------------------------------------------------------------------------- /public/assets/jquery: -------------------------------------------------------------------------------- 1 | ../../vendor/components/jquery/ -------------------------------------------------------------------------------- /public/assets/js/bootstrap-slider.min.js: -------------------------------------------------------------------------------- 1 | /*! ======================================================= 2 | VERSION 6.0.6 3 | ========================================================= */ 4 | "use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol?"symbol":typeof a};/*! ========================================================= 5 | * bootstrap-slider.js 6 | * 7 | * Maintainers: 8 | * Kyle Kemp 9 | * - Twitter: @seiyria 10 | * - Github: seiyria 11 | * Rohit Kalkur 12 | * - Twitter: @Rovolutionary 13 | * - Github: rovolution 14 | * 15 | * ========================================================= 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software 24 | * distributed under the License is distributed on an "AS IS" BASIS, 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | * See the License for the specific language governing permissions and 27 | * limitations under the License. 28 | * ========================================================= */ 29 | !function(a){if("function"==typeof define&&define.amd)define(["jquery"],a);else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var b;try{b=require("jquery")}catch(c){b=null}module.exports=a(b)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function c(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var f=Object.keys(this.defaultOptions),g=0;g0){for(g=0;g0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",g=0;g0&&(this.options.max=Math.max.apply(Math,this.options.ticks),this.options.min=Math.min.apply(Math,this.options.ticks)),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=k||this.trackLow,this.trackSelection=j||this.trackSelection,this.trackHigh=l||this.trackHigh,"none"===this.options.selection&&(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")),this.handle1=m||this.handle1,this.handle2=n||this.handle2,p===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),g=0;g0){for(var d,e,f,g=0,h=1;hthis.options.max?this.options.max:k},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=0===this.options.min?0:Math.log(this.options.min),c=Math.log(this.options.max),d=Math.exp(b+(c-b)*a/100);return d=this.options.min+Math.round((d-this.options.min)/this.options.step)*this.options.step,dthis.options.max?this.options.max:d},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=Math.log(this.options.max),c=0===this.options.min?0:Math.log(this.options.min),d=0===a?0:Math.log(a);return 100*(d-c)/(b-c)}}};if(b=function(a,b){return c.call(this,a,b),this},b.prototype={_init:function(){},constructor:b,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,handle:"round",reversed:!1,enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,scale:"linear",focus:!1,tooltip_position:null,labelledby:null},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];return b===!0&&this._trigger("slide",f),d!==f&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this._setDataVal(f),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),this.$element.removeData("slider"))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(){return this._removeSliderEventHandlers(),c.call(this,this.element,this.options),a&&a.data(this.element,"slider",this),this},relayout:function(){return this._layout(),this},_removeSliderEventHandlers:function(){this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.showTooltip&&(this.handle1.removeEventListener("focus",this.showTooltip,!1),this.handle2.removeEventListener("focus",this.showTooltip,!1)),this.hideTooltip&&(this.handle1.removeEventListener("blur",this.hideTooltip,!1),this.handle2.removeEventListener("blur",this.hideTooltip,!1)),this.showTooltip&&this.sliderElem.removeEventListener("mouseenter",this.showTooltip,!1),this.hideTooltip&&this.sliderElem.removeEventListener("mouseleave",this.hideTooltip,!1),this.sliderElem.removeEventListener("touchstart",this.mousedown,!1),this.sliderElem.removeEventListener("mousedown",this.mousedown,!1),window.removeEventListener("resize",this.resize,!1)},_bindNonQueryEventHandler:function(a,b){void 0===this.eventToCallbackMap[a]&&(this.eventToCallbackMap[a]=[]),this.eventToCallbackMap[a].push(b)},_unbindNonQueryEventHandler:function(a,b){var c=this.eventToCallbackMap[a];if(void 0!==c)for(var d=0;d0){var b="vertical"===this.options.orientation?"height":"width",c="vertical"===this.options.orientation?"marginTop":"marginLeft",d=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var e=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[c]=-d/2+"px"),e=this.tickLabelContainer.offsetHeight;else for(f=0;fe&&(e=this.tickLabelContainer.childNodes[f].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=e+"px")}for(var f=0;f=a[0]&&g<=a[1]&&this._addClass(this.ticks[f],"in-selection"):"after"===this.options.selection&&g>=a[0]?this._addClass(this.ticks[f],"in-selection"):"before"===this.options.selection&&g<=a[0]&&this._addClass(this.ticks[f],"in-selection"),this.tickLabels[f]&&(this.tickLabels[f].style[b]=d+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[f]?(this.tickLabels[f].style.position="absolute",this.tickLabels[f].style[this.stylePos]=g+"%",this.tickLabels[f].style[c]=-d/2+"px"):"vertical"===this.options.orientation&&(this.tickLabels[f].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style.marginTop=this.sliderElem.offsetWidth/2*-1+"px"))}}var h;if(this.options.range){h=this.options.formatter(this._state.value),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px"),"vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");var i=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,i);var j=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,j),this.tooltip_min.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip_min,"margin-top",-this.tooltip_min.offsetHeight/2+"px"):this._css(this.tooltip_min,"margin-left",-this.tooltip_min.offsetWidth/2+"px"),this.tooltip_max.style[this.stylePos]=a[1]+"%","vertical"===this.options.orientation?this._css(this.tooltip_max,"margin-top",-this.tooltip_max.offsetHeight/2+"px"):this._css(this.tooltip_max,"margin-left",-this.tooltip_max.offsetWidth/2+"px")}else h=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%",this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var k=this.tooltip_min.getBoundingClientRect(),l=this.tooltip_max.getBoundingClientRect();k.right>l.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this._layout(),this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this._setDataVal(e),this.setValue(e,!1,!0),this._pauseEvent(a),this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="vertical"===this.options.orientation&&!this.options.reversed,e="horizontal"===this.options.orientation&&this.options.reversed;(d||e)&&(c=-c)}var f=this._state.value[a]+c*this.options.step;return this.options.range&&(f=[a?this._state.value[0]:f,a?f:this._state.value[1]]),this._trigger("slideStart",f),this._setDataVal(f),this.setValue(f,!0,!0),this._setDataVal(f),this._trigger("slideStop",f),this._layout(),this._pauseEvent(b),!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this._layout();var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)c&&(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0)}},_mouseup:function(){if(!this._state.enabled)return!1;this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var a=this._calculateValue(!0);return this._layout(),this._setDataVal(a),this._trigger("slideStop",a),!1},_calculateValue:function(a){var b;if(this.options.range?(b=[this.options.min,this.options.max],0!==this._state.percentage[0]&&(b[0]=this._toValue(this._state.percentage[0]),b[0]=this._applyPrecision(b[0])),100!==this._state.percentage[1]&&(b[1]=this._toValue(this._state.percentage[1]),b[1]=this._applyPrecision(b[1]))):(b=this._toValue(this._state.percentage[0]),b=parseFloat(b),b=this._applyPrecision(b)),a){for(var c=[b,1/0],d=0;d 0) { 160 | this.$element.data('active', items[0]); 161 | } else { 162 | this.$element.data('active', null); 163 | } 164 | 165 | // Add item 166 | if (this.options.addItem){ 167 | items.push(this.options.addItem); 168 | } 169 | 170 | if (this.options.items == 'all') { 171 | return this.render(items).show(); 172 | } else { 173 | return this.render(items.slice(0, this.options.items)).show(); 174 | } 175 | }, 176 | 177 | matcher: function (item) { 178 | var it = this.displayText(item); 179 | return ~it.toLowerCase().indexOf(this.query.toLowerCase()); 180 | }, 181 | 182 | sorter: function (items) { 183 | var beginswith = [] 184 | , caseSensitive = [] 185 | , caseInsensitive = [] 186 | , item; 187 | 188 | while ((item = items.shift())) { 189 | var it = this.displayText(item); 190 | if (!it.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item); 191 | else if (~it.indexOf(this.query)) caseSensitive.push(item); 192 | else caseInsensitive.push(item); 193 | } 194 | 195 | return beginswith.concat(caseSensitive, caseInsensitive); 196 | }, 197 | 198 | highlighter: function (item) { 199 | var html = $('
'); 200 | var query = this.query; 201 | var i = item.toLowerCase().indexOf(query.toLowerCase()); 202 | var len, leftPart, middlePart, rightPart, strong; 203 | len = query.length; 204 | if(len === 0){ 205 | return html.text(item).html(); 206 | } 207 | while (i > -1) { 208 | leftPart = item.substr(0, i); 209 | middlePart = item.substr(i, len); 210 | rightPart = item.substr(i + len); 211 | strong = $('').text(middlePart); 212 | html 213 | .append(document.createTextNode(leftPart)) 214 | .append(strong); 215 | item = rightPart; 216 | i = item.toLowerCase().indexOf(query.toLowerCase()); 217 | } 218 | return html.append(document.createTextNode(item)).html(); 219 | }, 220 | 221 | render: function (items) { 222 | var that = this; 223 | var self = this; 224 | var activeFound = false; 225 | items = $(items).map(function (i, item) { 226 | var text = self.displayText(item); 227 | i = $(that.options.item).data('value', item); 228 | i.find('a').html(that.highlighter(text)); 229 | if (text == self.$element.val()) { 230 | i.addClass('active'); 231 | self.$element.data('active', item); 232 | activeFound = true; 233 | } 234 | return i[0]; 235 | }); 236 | 237 | if (this.autoSelect && !activeFound) { 238 | items.first().addClass('active'); 239 | this.$element.data('active', items.first().data('value')); 240 | } 241 | this.$menu.html(items); 242 | return this; 243 | }, 244 | 245 | displayText: function(item) { 246 | return item.name || item; 247 | }, 248 | 249 | next: function (event) { 250 | var active = this.$menu.find('.active').removeClass('active') 251 | , next = active.next(); 252 | 253 | if (!next.length) { 254 | next = $(this.$menu.find('li')[0]); 255 | } 256 | 257 | next.addClass('active'); 258 | }, 259 | 260 | prev: function (event) { 261 | var active = this.$menu.find('.active').removeClass('active') 262 | , prev = active.prev(); 263 | 264 | if (!prev.length) { 265 | prev = this.$menu.find('li').last(); 266 | } 267 | 268 | prev.addClass('active'); 269 | }, 270 | 271 | listen: function () { 272 | this.$element 273 | .on('focus', $.proxy(this.focus, this)) 274 | .on('blur', $.proxy(this.blur, this)) 275 | .on('keypress', $.proxy(this.keypress, this)) 276 | .on('keyup', $.proxy(this.keyup, this)); 277 | 278 | if (this.eventSupported('keydown')) { 279 | this.$element.on('keydown', $.proxy(this.keydown, this)); 280 | } 281 | 282 | this.$menu 283 | .on('click', $.proxy(this.click, this)) 284 | .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) 285 | .on('mouseleave', 'li', $.proxy(this.mouseleave, this)); 286 | }, 287 | 288 | destroy : function () { 289 | this.$element.data('typeahead',null); 290 | this.$element.data('active',null); 291 | this.$element 292 | .off('focus') 293 | .off('blur') 294 | .off('keypress') 295 | .off('keyup'); 296 | 297 | if (this.eventSupported('keydown')) { 298 | this.$element.off('keydown'); 299 | } 300 | 301 | this.$menu.remove(); 302 | }, 303 | 304 | eventSupported: function(eventName) { 305 | var isSupported = eventName in this.$element; 306 | if (!isSupported) { 307 | this.$element.setAttribute(eventName, 'return;'); 308 | isSupported = typeof this.$element[eventName] === 'function'; 309 | } 310 | return isSupported; 311 | }, 312 | 313 | move: function (e) { 314 | if (!this.shown) return; 315 | 316 | switch(e.keyCode) { 317 | case 9: // tab 318 | case 13: // enter 319 | case 27: // escape 320 | e.preventDefault(); 321 | break; 322 | 323 | case 38: // up arrow 324 | // with the shiftKey (this is actually the left parenthesis) 325 | if (e.shiftKey) return; 326 | e.preventDefault(); 327 | this.prev(); 328 | break; 329 | 330 | case 40: // down arrow 331 | // with the shiftKey (this is actually the right parenthesis) 332 | if (e.shiftKey) return; 333 | e.preventDefault(); 334 | this.next(); 335 | break; 336 | } 337 | }, 338 | 339 | keydown: function (e) { 340 | this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]); 341 | if (!this.shown && e.keyCode == 40) { 342 | this.lookup(); 343 | } else { 344 | this.move(e); 345 | } 346 | }, 347 | 348 | keypress: function (e) { 349 | if (this.suppressKeyPressRepeat) return; 350 | this.move(e); 351 | }, 352 | 353 | keyup: function (e) { 354 | switch(e.keyCode) { 355 | case 40: // down arrow 356 | case 38: // up arrow 357 | case 16: // shift 358 | case 17: // ctrl 359 | case 18: // alt 360 | break; 361 | 362 | case 9: // tab 363 | case 13: // enter 364 | if (!this.shown) return; 365 | this.select(); 366 | break; 367 | 368 | case 27: // escape 369 | if (!this.shown) return; 370 | this.hide(); 371 | break; 372 | default: 373 | this.lookup(); 374 | } 375 | 376 | e.preventDefault(); 377 | }, 378 | 379 | focus: function (e) { 380 | if (!this.focused) { 381 | this.focused = true; 382 | if (this.options.showHintOnFocus) { 383 | this.lookup(''); 384 | } 385 | } 386 | }, 387 | 388 | blur: function (e) { 389 | this.focused = false; 390 | if (!this.mousedover && this.shown) this.hide(); 391 | }, 392 | 393 | click: function (e) { 394 | e.preventDefault(); 395 | this.select(); 396 | this.$element.focus(); 397 | }, 398 | 399 | mouseenter: function (e) { 400 | this.mousedover = true; 401 | this.$menu.find('.active').removeClass('active'); 402 | $(e.currentTarget).addClass('active'); 403 | }, 404 | 405 | mouseleave: function (e) { 406 | this.mousedover = false; 407 | if (!this.focused && this.shown) this.hide(); 408 | } 409 | 410 | }; 411 | 412 | 413 | /* TYPEAHEAD PLUGIN DEFINITION 414 | * =========================== */ 415 | 416 | var old = $.fn.typeahead; 417 | 418 | $.fn.typeahead = function (option) { 419 | var arg = arguments; 420 | if (typeof option == 'string' && option == 'getActive') { 421 | return this.data('active'); 422 | } 423 | return this.each(function () { 424 | var $this = $(this) 425 | , data = $this.data('typeahead') 426 | , options = typeof option == 'object' && option; 427 | if (!data) $this.data('typeahead', (data = new Typeahead(this, options))); 428 | if (typeof option == 'string') { 429 | if (arg.length > 1) { 430 | data[option].apply(data, Array.prototype.slice.call(arg ,1)); 431 | } else { 432 | data[option](); 433 | } 434 | } 435 | }); 436 | }; 437 | 438 | $.fn.typeahead.defaults = { 439 | source: [] 440 | , items: 8 441 | , menu: '' 442 | , item: '
  • ' 443 | , minLength: 1 444 | , scrollHeight: 0 445 | , autoSelect: true 446 | , afterSelect: $.noop 447 | , addItem: false 448 | , delay: 0 449 | }; 450 | 451 | $.fn.typeahead.Constructor = Typeahead; 452 | 453 | 454 | /* TYPEAHEAD NO CONFLICT 455 | * =================== */ 456 | 457 | $.fn.typeahead.noConflict = function () { 458 | $.fn.typeahead = old; 459 | return this; 460 | }; 461 | 462 | 463 | /* TYPEAHEAD DATA-API 464 | * ================== */ 465 | 466 | $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 467 | var $this = $(this); 468 | if ($this.data('typeahead')) return; 469 | $this.typeahead($this.data()); 470 | }); 471 | 472 | })); 473 | 474 | -------------------------------------------------------------------------------- /public/assets/js/gcc.js: -------------------------------------------------------------------------------- 1 | function isNumber(n) { 2 | return /^-?[\d.]+(?:e-?\d+)?$/.test(n); 3 | } 4 | 5 | function bytesToSize(bytes) { 6 | var sizes = ['MB', 'GB', 'TB']; 7 | if (bytes == 0) return 'n/a'; 8 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); 9 | return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[[i]]; 10 | } 11 | 12 | function updateJobStatusModal() { 13 | var jobId = $( "#jobStatusModal" ).data( "jobId" ); 14 | $.getJSON("/jobStatus/" + jobId , function(data) { 15 | $( "#jobStatusModal").find( ".modal-body" ).html( "

    Current Job State: " + data + "

    " ); 16 | if (data != "success" && data != "error") { 17 | setTimeout(updateJobStatusModal, 1000); 18 | } 19 | else { 20 | if (data == "success") { 21 | setTimeout(function() { 22 | $( "#jobStatusModal").modal('hide'); 23 | location.reload(); 24 | }, 1000); 25 | } 26 | else { 27 | $( "#jobStatusLoader" ).remove(); 28 | } 29 | } 30 | }); 31 | } 32 | 33 | function processResult(data) { 34 | if( isNumber(data) && data > 0) { 35 | $( "#jobStatusModal" ).find( ".modal-title" ).text( "Job Status for #" + data ); 36 | $( "#jobStatusModal" ).data( "jobId", data); 37 | $( "#jobStatusModal").find( ".modal-body" ).html( "

    " ); 38 | setTimeout(updateJobStatusModal, 1000); 39 | $( "#jobStatusModal" ).modal(); 40 | } 41 | else { 42 | $( "#jobStatusModal").find( ".modal-title" ).text( "Error" ); 43 | $( "#jobStatusModal").data( "jobId", '-1'); 44 | if(data.message) { 45 | var errorMsg = "Error-Code: " + data.code + "
    \n" + "Message: " + data.message; 46 | } 47 | else { 48 | var errorMsg = "The request failed (unknown reason)"; 49 | } 50 | $( "#jobStatusModal").find( ".modal-body" ).html( "
    " + errorMsg + "
    " ); 51 | $( "#jobStatusModal").modal(); 52 | } 53 | } 54 | 55 | function setInstanceParameter(instance, type, param, value) { 56 | $.post ( "/instanceParameter/" + instance + "/" + type + "/" + param + "/" + value, null, processResult, 'json'); 57 | } 58 | 59 | function changeInstanceStatus(instance, instanceAction) { 60 | $.post ( "/changeInstanceStatus/" + instance, { 'action': instanceAction }, processResult, 'json') 61 | } 62 | 63 | function updateNic(instance, nicId, mac, link) { 64 | $.post ( "/updateNic/" + instance + "/" + nicId + "/" + mac + "/" + link, null, processResult, 'json') 65 | } 66 | 67 | function addNic(instance, mac, link) { 68 | $.post ( "/addNic/" + instance + "/" + mac + "/" + link, null, processResult, 'json') 69 | } 70 | 71 | function growDisk(instance, diskId, amount) { 72 | $.post ( "/growDisk/" + instance + "/" + diskId + "/" + amount, null, processResult, 'json') 73 | } 74 | 75 | function addDisk(instance, size) { 76 | $.post ( "/addDisk/" + instance + "/" + size, null, processResult, 'json') 77 | } 78 | 79 | function addTag(instance, tag) { 80 | $.post ( "/addTag/" + instance + "/" + tag, null, processResult, 'json') 81 | } 82 | 83 | function setClusterHvParameter(type, param, value) { 84 | $.post ( "/clusterHvParameter/" + type + "/" + param + "/" + value, null, processResult, 'json'); 85 | } 86 | 87 | function setClusterIpolicyParameter(param, value) { 88 | $.post ( "/clusterIpolicyParameter/" + param + "/" + value, null, processResult, 'json'); 89 | } 90 | 91 | $( document ).ready(function() { 92 | $( ".paramChange" ).click(function() { 93 | var instance = $(this).data("instance"); 94 | var type = $(this).data("type"); 95 | var param = $(this).data("param"); 96 | var value = $(this).data("value"); 97 | setInstanceParameter(instance, type, param, value); 98 | }); 99 | 100 | $( ".instanceAction" ).click(function() { 101 | var instance = $(this).data("instance"); 102 | var instanceAction = $(this).data("instanceaction"); 103 | $.confirm({ 104 | text: "Are you sure you want to perform the action '" + instanceAction + "' on Instance '" + instance + "'?", 105 | confirm: function() { changeInstanceStatus(instance, instanceAction); }, 106 | }); 107 | 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /public/assets/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | // See the Getting Started docs for more information: 8 | // http://getbootstrap.com/getting-started/#support-ie10-width 9 | 10 | (function () { 11 | 'use strict'; 12 | 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /public/assets/js/jquery.confirm.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jquery.confirm 3 | * 4 | * @version 2.3.1 5 | * 6 | * @author My C-Labs 7 | * @author Matthieu Napoli 8 | * @author Russel Vela 9 | * @author Marcus Schwarz 10 | * 11 | * @license MIT 12 | * @url https://myclabs.github.io/jquery.confirm/ 13 | */ 14 | (function(a){a.fn.confirm=function(b){if(typeof b==="undefined"){b={}}this.click(function(c){c.preventDefault();var d=a.extend({button:a(this)},b);a.confirm(d,c)});return this};a.confirm=function(f,i){if(a(".confirmation-modal").length>0){return}var j={};if(f.button){var b={title:"title",text:"text","confirm-button":"confirmButton","cancel-button":"cancelButton","confirm-button-class":"confirmButtonClass","cancel-button-class":"cancelButtonClass","dialog-class":"dialogClass"};a.each(b,function(e,k){var l=f.button.data(e);if(l){j[k]=l}})}var g=a.extend({},a.confirm.options,{confirm:function(){var e=i&&(("string"===typeof i&&i)||(i.currentTarget&&i.currentTarget.attributes.href.value));if(e){if(f.post){var k=a('
    ');a("body").append(k);k.submit()}else{window.location=e}}},cancel:function(e){},button:null},j,f);var c="";if(g.title!==""){c='"}var d='";var h=a(d);h.on("shown.bs.modal",function(){h.find(".btn-primary:first").focus()});h.on("hidden.bs.modal",function(){h.remove()});h.find(".confirm").click(function(){g.confirm(g.button)});h.find(".cancel").click(function(){g.cancel(g.button)});a("body").append(h);h.modal("show")};a.confirm.options={text:"Are you sure?",title:"",confirmButton:"Yes",cancelButton:"Cancel",post:false,confirmButtonClass:"btn-primary",cancelButtonClass:"btn-default",dialogClass:"modal-dialog"}})(jQuery); -------------------------------------------------------------------------------- /public/assets/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | '../tpl' 29 | )); 30 | 31 | $app->view(new \Slim\Views\Twig()); 32 | $app->view->parserExtensions = array(new \Slim\Views\TwigExtension()); 33 | 34 | $clusterIndex = $app->getCookie('clusterIndex'); 35 | if(!isset($clusterIndex) || !isset($config["rapi"][$clusterIndex])) { 36 | $clusterIndex = 0; 37 | $app->setCookie('clusterIndex', '0', '365 days'); 38 | } 39 | $config["rapi-current"] = $config["rapi"][$clusterIndex]; 40 | 41 | 42 | $app->get('/', function() use ($app) { 43 | $app->redirect("/instances"); 44 | }); 45 | 46 | $app->get('/instances(/:filter(/:value))', function($filter = "none", $value = "") use ($app) { 47 | global $config; 48 | $g = new ganetiClient($config["rapi-current"]); 49 | $cluster = $g->getClusterInfo(); 50 | $clusterName = $g->getConfigName(); 51 | $instances = $g->getInstances(true); 52 | $instancesFiltered = array(); 53 | switch($filter) { 54 | case "byPrimaryNode": 55 | for($i = 0; $i < count($instances); $i++) { 56 | if($instances[$i]["pnode"] == $value) { 57 | $instancesFiltered[] = $instances[$i]; 58 | } 59 | } 60 | $instances = $instancesFiltered; 61 | break; 62 | case "bySecondaryNode": 63 | for($i = 0; $i < count($instances); $i++) { 64 | if($instances[$i]["snodes"][0] == $value) { 65 | $instancesFiltered[] = $instances[$i]; 66 | } 67 | } 68 | $instances = $instancesFiltered; 69 | break; 70 | } 71 | $app->render('page_instances.html', array( "config" => $config, 72 | "clusterName" => $clusterName, 73 | "cluster" => $cluster, 74 | "instances" => $instances, 75 | )); 76 | }); 77 | 78 | $app->get('/instanceDetails/:h', function($instanceName) use ($app) { 79 | global $config; 80 | $g = new ganetiClient($config["rapi-current"]); 81 | $clusterName = $g->getConfigName(); 82 | $instance = $g->getInstance($instanceName); 83 | $pageTitle = $instanceName; 84 | if(isset($instance["nic.macs"][0])) { 85 | $mac = $instance["nic.macs"][0]; 86 | $filename = "01-" . str_replace(":","-",$mac); 87 | $preseedConfigExists = file_exists("/var/lib/tftpboot/pxelinux.cfg/" . $filename); 88 | } 89 | else { 90 | $preseedConfigExists = false; 91 | } 92 | $app->render('page_instancedetails.html', array( "config" => $config, 93 | "clusterName" => $clusterName, 94 | "pageTitle" => $pageTitle, 95 | "instance" => $instance, 96 | "instance_dump" => print_r($instance,true), 97 | "preseedConfigExists" => $preseedConfigExists, 98 | "vlans" => $config["vlans"][$clusterName], 99 | "spiceCa" => $config["rapi-current"]["spice-ca"], 100 | )); 101 | }); 102 | 103 | $app->get('/clusterNodes', function() use ($app) { 104 | global $config; 105 | $g = new ganetiClient($config["rapi-current"]); 106 | $cluster = $g->getClusterInfo(); 107 | $clusterName = $g->getConfigName(); 108 | $nodes = $g->getNodes(true); 109 | $groups = $g->getGroups(true); 110 | for($i = 0; $i < count($nodes); $i++) { 111 | if(!empty($nodes[$i]["group.uuid"])) { 112 | foreach($groups AS $group) { 113 | if($group["uuid"] == $nodes[$i]["group.uuid"]) { 114 | $nodes[$i]["group.name"] = $group["name"]; 115 | $nodes[$i]["group.policy"] = $group["alloc_policy"]; 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | $app->render('page_clusternodes.html', array( "config" => $config, 122 | "clusterName" => $clusterName, 123 | "cluster" => $cluster, 124 | "nodes" => $nodes, 125 | "groups" => $groups, 126 | )); 127 | }); 128 | 129 | $app->get('/jobs', function() use ($app) { 130 | global $config; 131 | $g = new ganetiClient($config["rapi-current"]); 132 | $cluster = $g->getClusterInfo(); 133 | $clusterName = $g->getConfigName(); 134 | $jobs = array_reverse($g->getJobs(true)); 135 | $app->render('page_jobs.html', array( "config" => $config, 136 | "clusterName" => $clusterName, 137 | "cluster" => $cluster, 138 | "jobs" => $jobs)); 139 | }); 140 | 141 | $app->get('/jobDetails/:h', function($jobid) use ($app) { 142 | global $config; 143 | $g = new ganetiClient($config["rapi-current"]); 144 | $clusterName = $g->getConfigName(); 145 | $job = $g->getJob($jobid); 146 | $app->render('page_jobdetails.html', array( "config" => $config, 147 | "clusterName" => $clusterName, 148 | "job" => $job, 149 | "job_dump" => print_r($job,true))); 150 | }); 151 | 152 | $app->get('/jobStatus', function() use ($app) { 153 | global $config; 154 | $g = new ganetiClient($config["rapi-current"]); 155 | $clusterName = $g->getConfigName(); 156 | $jobs = array_reverse($g->getJobs(true)); 157 | $status = array(); 158 | foreach($jobs AS $job) { 159 | $status[$job["id"]] = $job["status"]; 160 | } 161 | Header("Content-Type: application/json"); 162 | echo json_encode($status); 163 | }); 164 | 165 | $app->get('/jobStatus/:j', function($jobId) use ($app) { 166 | global $config; 167 | $g = new ganetiClient($config["rapi-current"]); 168 | $clusterName = $g->getConfigName(); 169 | $job = $g->getJob($jobId); 170 | $status = $job["status"]; 171 | Header("Content-Type: application/json"); 172 | echo json_encode($status); 173 | }); 174 | 175 | $app->get('/createInstance', function() use ($app) { 176 | global $config; 177 | $g = new ganetiClient($config["rapi-current"]); 178 | $cluster = $g->getClusterInfo(); 179 | $clusterName = $g->getConfigName(); 180 | $app->render('page_createinstance.html', array( "config" => $config, 181 | "clusterName" => $clusterName, 182 | "vlans" => $config["vlans"][$clusterName], 183 | "cluster" => $cluster, 184 | )); 185 | }); 186 | 187 | $app->post('/createInstance', function() use ($app) { 188 | global $config; 189 | $g = new ganetiClient($config["rapi-current"]); 190 | 191 | $params = array( 192 | "iallocator" => "hail", 193 | ); 194 | $instances = array(); 195 | $instanceTags = array(); 196 | if(isset($_POST["distributeInstances"]) && $_POST["distributeInstances"] === "true") { 197 | $instanceTags[] = $config["rapi-current"]["exclusion-tag"] . ":" . uniqid(); 198 | } 199 | if(!empty($_POST["instanceTag"])) { 200 | $instanceTags[] = $_POST["instanceTag"]; 201 | } 202 | foreach($_POST["vmName"] AS $instanceName) { 203 | $instances[] = array( 204 | "conflicts_check" => false, 205 | "disk_template" => "drbd", 206 | "ip_check" => false, 207 | "mode" => "create", 208 | "name_check" => false, 209 | "no_install" => $config["rapi-current"]["auto-install-instance"], 210 | "wait_for_sync" => false, 211 | "instance_name" => $instanceName, 212 | "os_type" => $config["rapi-current"]["default-os"], 213 | "hypervisor" => "kvm", 214 | "beparams" => array( 215 | "vcpus" => $_POST["cpuCount"], 216 | "memory" => $_POST["memory"], 217 | ), 218 | "nics" => array( 219 | array( 220 | "link" => "br" . $_POST["vlan"], 221 | "mac" => "generate" 222 | ), 223 | ), 224 | "disks" => array( 225 | array( 226 | "size" => ($_POST["diskSpace"]*1024) 227 | ), 228 | ), 229 | "hvparams" => array( 230 | "kernel_path" => "", 231 | "machine_version" => "pc", 232 | "nic_type" => "paravirtual" 233 | ), 234 | ); 235 | if(!empty($instanceTags)) { 236 | $key = count($instances) - 1; 237 | $instances[$key]["tags"] = $instanceTags; 238 | } 239 | } 240 | $params["instances"] = $instances; 241 | $g->createMultiInstance($params); 242 | $app->redirect("/jobs"); 243 | }); 244 | 245 | $app->post('/changeInstanceStatus/:h', function($instance) use ($app) { 246 | global $config; 247 | $g = new ganetiClient($config["rapi-current"]); 248 | $return = -1; 249 | switch($_POST["action"]) { 250 | case "start": 251 | $return = $g->startInstance($instance); 252 | break; 253 | case "shutdown": 254 | $return = $g->shutdownInstance($instance); 255 | break; 256 | case "kill": 257 | $return = $g->shutdownInstance($instance,0); 258 | break; 259 | case "migrate": 260 | $return = $g->migrateInstance($instance); 261 | break; 262 | case "preseed": 263 | $inst = $g->getInstance($instance); 264 | if(!$inst["oper_state"]) { 265 | $mac = $inst["nic.macs"][0]; 266 | $filename = "01-" . str_replace(":","-",$mac); 267 | if(copy("/etc/ganeti-control-center/pxe-boot-template.txt","/var/lib/tftpboot/pxelinux.cfg/" . $filename)) { 268 | $return = $g->startInstance($instance); 269 | } 270 | } 271 | break; 272 | } 273 | Header("Content-Type: application/json"); 274 | echo json_encode($return); 275 | exit; 276 | }); 277 | 278 | $app->get('/removePreseedConfig/:h', function($instance) use ($app) { 279 | global $config; 280 | $g = new ganetiClient($config["rapi-current"]); 281 | $inst = $g->getInstance($instance); 282 | $mac = $inst["nic.macs"][0]; 283 | $filename = "01-" . str_replace(":","-",$mac); 284 | if(file_exists("/var/lib/tftpboot/pxelinux.cfg/" . $filename)) { 285 | unlink("/var/lib/tftpboot/pxelinux.cfg/" . $filename); 286 | } 287 | $app->redirect("/instanceDetails/" . $instance); 288 | }); 289 | 290 | $app->get('/setCluster/:h', function($cluster) use ($app) { 291 | global $config; 292 | if(is_numeric($cluster) && isset($config["rapi"][$cluster])) { 293 | $app->setCookie("clusterIndex", $cluster, "365 days"); 294 | } 295 | else { 296 | $app->setCookie("clusterIndex", "0", "365 days"); 297 | } 298 | $app->redirect("/instances"); 299 | exit; 300 | }); 301 | 302 | $app->get('/getSpiceConfig/:i', function($instance) use ($app) { 303 | global $config; 304 | $g = new ganetiClient($config["rapi-current"]); 305 | $inst = $g->getInstance($instance); 306 | 307 | Header("Content-Type: application/x-virt-viewer"); 308 | header("Content-disposition: attachment; filename=spice-connection.vv"); 309 | echo "[virt-viewer]\n"; 310 | echo "type=spice\n"; 311 | echo "host=" . $inst["pnode"] . "\n"; 312 | if($inst["hvparams"]["spice_use_tls"]) { 313 | echo "tls-port=" . $inst["network_port"] . "\n"; 314 | echo "host-subject=CN=ganeti.example.com\n"; 315 | } 316 | else { 317 | echo "port=" . $inst["network_port"] . "\n"; 318 | } 319 | echo "delete-this-file=1\n"; 320 | exit; 321 | }); 322 | 323 | $app->post('/instanceParameter/:h/:t/:p/:v', function($instance, $type, $parameter, $value) use ($app) { 324 | global $config; 325 | $g = new ganetiClient($config["rapi-current"]); 326 | $return = -1; 327 | switch($type) { 328 | case "beparam": 329 | switch($parameter) { 330 | case "autoBalance": 331 | switch($value) { 332 | case "0": 333 | $return = $g->setInstanceParameter($instance, $type, "auto_balance", false); 334 | break; 335 | case "1": 336 | $return = $g->setInstanceParameter($instance, $type, "auto_balance", true); 337 | break; 338 | } 339 | break; 340 | case "spindleUseCount": 341 | if(is_numeric($value)) { 342 | $return = $g->setInstanceParameter($instance, $type, "spindle_use", $value); 343 | } 344 | break; 345 | case "cpuCount": 346 | if(is_numeric($value)) { 347 | $return = $g->setInstanceParameter($instance, $type, "vcpus", $value); 348 | } 349 | break; 350 | case "memoryAmount": 351 | if(is_numeric($value)) { 352 | $beparams = array( 353 | "memory" => $value, 354 | "minmem" => $value, 355 | "maxmem" => $value, 356 | ); 357 | $return = $g->setInstanceParameters($instance, $beparams); 358 | } 359 | break; 360 | } 361 | break; 362 | case "hvparam": 363 | $return = $g->setInstanceParameter($instance, $type, $parameter, $value); 364 | break; 365 | } 366 | Header("Content-Type: application/json"); 367 | echo json_encode($return); 368 | exit; 369 | }); 370 | 371 | $app->post('/updateNic/:i/:n/:m/:l', function($instance, $nic, $mac, $link) use ($app) { 372 | global $config; 373 | $g = new ganetiClient($config["rapi-current"]); 374 | $return = $g->setNicParameters($instance, $nic, $mac, $link); 375 | Header("Content-Type: application/json"); 376 | echo json_encode($return); 377 | exit; 378 | }); 379 | 380 | $app->post('/addNic/:i/:m/:l', function($instance, $mac, $link) use ($app) { 381 | global $config; 382 | $g = new ganetiClient($config["rapi-current"]); 383 | $return = $g->addNic($instance, $mac, $link); 384 | Header("Content-Type: application/json"); 385 | echo json_encode($return); 386 | exit; 387 | }); 388 | 389 | $app->post('/growDisk/:i/:d/:a', function($instance, $disk, $amount) use ($app) { 390 | global $config; 391 | $g = new ganetiClient($config["rapi-current"]); 392 | $amount = $amount * 1024; 393 | $return = $g->growDisk($instance, $disk, $amount); 394 | Header("Content-Type: application/json"); 395 | echo json_encode($return); 396 | exit; 397 | }); 398 | 399 | $app->post('/addDisk/:i/:s', function($instance, $size) use ($app) { 400 | global $config; 401 | $g = new ganetiClient($config["rapi-current"]); 402 | $return = $g->addDisk($instance, $size); 403 | Header("Content-Type: application/json"); 404 | echo json_encode($return); 405 | exit; 406 | }); 407 | 408 | $app->post('/addTag/:i/:s', function($instance, $tag) use ($app) { 409 | global $config; 410 | $g = new ganetiClient($config["rapi-current"]); 411 | $return = $g->addTag($instance, $tag); 412 | Header("Content-Type: application/json"); 413 | echo json_encode($return); 414 | exit; 415 | }); 416 | 417 | $app->post('/clusterHvParameter/:t/:p/:v', function($type, $parameter, $value) use ($app) { 418 | global $config; 419 | $g = new ganetiClient($config["rapi-current"]); 420 | $return = $g->setClusterParameter("hvparams", $type, $parameter, $value); 421 | Header("Content-Type: application/json"); 422 | echo json_encode($return); 423 | exit; 424 | }); 425 | 426 | $app->post('/clusterIpolicyParameter/:p/:v', function($parameter, $value) use ($app) { 427 | global $config; 428 | $g = new ganetiClient($config["rapi-current"]); 429 | $return = $g->setClusterParameter("ipolicy", "none", $parameter, $value); 430 | Header("Content-Type: application/json"); 431 | echo json_encode($return); 432 | exit; 433 | }); 434 | 435 | $app->get('/statistics', function() use ($app) { 436 | global $config; 437 | $g = new ganetiClient($config["rapi-current"]); 438 | $cluster = $g->getClusterInfo(); 439 | $clusterName = $g->getConfigName(); 440 | $stats = $g->getStats(); 441 | 442 | $app->render('page_statistics.html', array( "config" => $config, 443 | "clusterName" => $clusterName, 444 | "vlans" => $config["vlans"][$clusterName], 445 | "cluster" => $cluster, 446 | "stats" => $stats, 447 | )); 448 | }); 449 | 450 | 451 | $app->run(); 452 | 453 | ?> 454 | -------------------------------------------------------------------------------- /tpl/dialogs_cluster.html: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 | 61 | 62 | -------------------------------------------------------------------------------- /tpl/dialogs_instancedetails.html: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 79 | 80 | 81 | 112 | 113 | 114 | 144 | 145 | 146 | 176 | 177 | 207 | 208 | 209 | 251 | 252 | -------------------------------------------------------------------------------- /tpl/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% if pageTitle is defined %}{{ pageTitle }}{% else %}Ganeti Control Center - {{ clusterName }}{% endif %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 71 | 72 |
    73 |
    74 |
    75 | {% block content %}{% endblock %} 76 |
    77 |
    78 |
    79 | 80 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /tpl/page_clusternodes.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |
    4 |
    5 |

    Cluster Nodes

    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for item in nodes %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% endfor %} 30 | 31 |
    NamePrimary Inst.Secondary Inst.CPUsMemory (Total/Free)Disk (Total/Free)Group
    {{ item.name }}{{ item.pinst_cnt }}{{ item.sinst_cnt }}{{ item.ctotal }}{{ item.mtotal }}/{{ item.mfree }}{{ item.dtotal }}/{{ item.dfree }}{{ item['group.name']|default('none') }}
    32 |
    33 |
    34 |
    35 |
    36 |

    General Information

    37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
    ParameterValue
    Version{{ cluster.software_version }} ({{ cluster.architecture.1 }})
    Master{{ cluster.master }}
    Disk Types{{ cluster.enabled_disk_templates|join(', ') }}
    Hypervisors{{ cluster.enabled_hypervisors|join(', ') }}
    63 |

    Allocation Policy

    64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 102 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 117 | 118 | 119 |
    ParameterValue
    vCPUs per real CPU 73 | {{ cluster.ipolicy['vcpu-ratio'] }} 74 |
    75 | 76 |
    77 |
    VMs per Spindle 82 | {{ cluster.ipolicy['spindle-ratio'] }} 83 |
    84 | 85 |
    86 |
    Max CPUs per VM 91 | {{ cluster.ipolicy['minmax'][0]['max']['cpu-count'] }} 92 |
    Max NICs per VM 97 | {{ cluster.ipolicy['minmax'][0]['max']['nic-count'] }} 98 |
    Max Disks per VM 103 | {{ cluster.ipolicy['minmax'][0]['max']['disk-count'] }} 104 |
    Max Disk Size per VM 109 | {{ cluster.ipolicy['minmax'][0]['max']['disk-size'] }}MB 110 |
    Max Memory Size per VM 115 | {{ cluster.ipolicy['minmax'][0]['max']['memory-size'] }}MB 116 |
    120 |

    KVM Migration Settings

    121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 135 | 136 | 137 | 138 | 144 | 145 | 146 |
    ParameterValue
    max. Memory Transfer Bandwidth 130 | {{ cluster.hvparams.kvm.migration_bandwidth }} MB/s 131 |
    132 | 133 |
    134 |
    max. Migration Downtime 139 | {{ cluster.hvparams.kvm.migration_downtime }} ms 140 |
    141 | 142 |
    143 |
    147 |
    148 |
    149 |

    DRBD Parameters

    150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | {% for key,value in cluster.diskparams.drbd %} 159 | 160 | 161 | 162 | 163 | {% endfor %} 164 | 165 |
    ParameterValue
    {{ key }}{{ value }}
    166 |
    167 |
    168 | {% include 'dialogs_cluster.html' %} 169 | 208 | 209 | {% endblock %} 210 | -------------------------------------------------------------------------------- /tpl/page_createinstance.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    Create one or more Instances

    4 |
    5 |
    6 |
    7 |
    8 | 9 | 10 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | 46 |
    47 |
    48 |
    49 |
    50 |
    51 | 52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 | {% for name,vlan in vlans %} 59 | 62 | {% endfor %} 63 |
    64 |
    65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 |
    72 | 102 | {% endblock %} 103 | -------------------------------------------------------------------------------- /tpl/page_instancedetails.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    Node Details for {{ instance.name }}

    4 | {% if preseedConfigExists %} {% endif %} 5 |
    6 |
    7 |
    8 | {% if instance.oper_state %} 9 |
    10 | Shutdown 11 | Kill 12 |
    13 |
    14 | Spice Console 15 | Client Setup 16 |
    17 |
    18 | Migrate 19 |
    20 | {% else %} 21 |
    22 | Start 23 |
    24 | 27 |
    28 |
    29 | {% endif %} 30 |
    31 |
    32 |
    33 |
    34 |
    35 |

    General Settings

    36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
    ParameterValue
    Primary Node{{ instance.pnode }}
    Secondary Node{{ instance.snodes.0 }}
    Default Disk Type 55 | {{ instance.hvparams.disk_type }} 56 |
    Default NIC Type 61 | {{ instance.hvparams.nic_type }} 62 |
    Spindle Use Count 67 | {{ instance.beparams.spindle_use }} 68 |
    69 | 70 | 71 |
    72 |
    Tags{% for tag in instance.tags %} {{ tag }} {% else %}no tags{% endfor %}
    80 |
    81 |
    82 |

    Memory/CPU

    83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 100 | 101 | 102 | 103 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
    ParameterValue
    vCPUs 94 | {{ instance.beparams.vcpus }} 95 |
    96 | 97 | 98 |
    99 |
    Memory 104 | {{ instance.beparams.memory / 1024 }} GB 105 |
    106 | 107 | 108 |
    109 |
    min. Memory{{ instance.beparams.minmem / 1024 }} GB
    max. Memory{{ instance.beparams.maxmem / 1024 }} GB
    121 |
    122 | 123 |
    124 |

    Network Settings

    125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | {% for key,mac in instance["nic.macs"] %} 136 | 137 | 138 | 139 | 140 | 146 | 147 | {% endfor %} 148 | 149 | 154 | 155 | 156 |
    #ModeMacLink
    {{ key }}{{ instance["nic.modes"][key] }}{{ mac }} 141 | {{ instance["nic.links"][key] }} 142 |
    143 | 144 |
    145 |
    150 |
    151 | 152 |
    153 |
    157 |

    Disk Settings

    158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | {% for key,size in instance["disk.sizes"] %} 167 | 168 | 169 | 170 | 175 | 176 | {% endfor %} 177 | 178 | 183 | 184 | 185 |
    #Size
    {{ key }}{{ size / 1024 }} GB 171 |
    172 | 173 |
    174 |
    179 |
    180 | 181 |
    182 |
    186 | 187 |
    188 |
    189 |

    Spice Settings

    190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 219 | 220 | 221 | 222 | 225 | 226 | 227 | 228 | 229 | 230 | 231 |
    ParameterValue
    Bind IP 201 | {{ instance.hvparams.spice_bind }} 202 |
    203 | 204 |
    205 |
    Port{{ instance.network_port }}
    TLS 214 | {{ instance.hvparams.spice_use_tls ? 'Yes' : 'No' }} 215 |
    216 | 217 |
    218 |
    Password enabled 223 | {{ instance.hvparams.spice_password_file ? 'Yes' : 'No' }} 224 |
    URLspice://{{ instance.pnode|length > 5 ? instance.pnode|slice(0, 5) ~ '...' : instance.pnode }}?{{ instance.hvparams.spice_use_tls ? 'tls-port' : 'port' }}={{ instance.network_port }}
    232 |
    233 |
    234 |
    235 |
    236 |

    Hypervisor Parameters

    237 |
    238 |
    239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | {% for key,value in instance.hvparams %} 248 | 249 | 250 | 251 | 252 | {% endfor %} 253 | 254 |
    ParameterValue
    {{ key }}{{ value }}
    255 |
    256 |
    257 |
    258 | 259 |
    260 | 261 | {% include 'dialogs_instancedetails.html' %} 262 | 352 | 353 | {% endblock %} 354 | -------------------------------------------------------------------------------- /tpl/page_instances.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    VMs on {{ cluster.name }}

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for item in instances %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | {% endfor %} 34 | 35 |
    NamePrimarySecondaryvCPUsMemoryControl
    {{ item.name }}{{ item.pnode }}{{ item.snodes[0] }}{{ item.beparams.vcpus }}{{ item.beparams.memory }} 24 | {% if item.oper_state %} 25 | Shutdown 26 | Migrate 27 | {% else %} 28 | Start 29 | {% endif %} 30 | 31 |
    36 | 59 | 60 | 61 | 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /tpl/page_jobdetails.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    Job Details for {{ job.id }} / {{ job.summary[0] }}

    4 | 28 |
    29 | {{ job_dump }}
    30 | 	
    31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /tpl/page_jobs.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    Jobs on {{ cluster.name }}

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for item in jobs %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
    IDResultSummaryStartEnd
    {{ item.id }}{{ item.opstatus[0] }}{{ item.summary[0] }}{% if item.instanceLink %}Instance Details{% endif %}{{ item.start_ts[0]|date("H:i:s d.m.y") }}{{ item.end_ts[0]|date("H:i:s d.m.y") }}
    28 | 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /tpl/page_statistics.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | {% block content%} 3 |

    Cluster Statistics

    4 |
    5 |
    6 |
    7 |
    8 |

    Nodes

    9 |
    10 |
    11 |

    {{ stats["node-count"] }}

    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |

    Instances

    19 |
    20 |
    21 |

    {{ stats["instance-count"] }}

    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |

    CPU Cores

    29 |
    30 |
    31 |

    {{ stats.nodes.cpus }}

    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |

    Disk Space

    39 |
    40 |
    41 |

    {{ stats.nodes.disk }}

    42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |

    Memory

    49 |
    50 |
    51 |

    {{ stats.nodes.memory }}

    52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 |

    Min/Avg/Max Inst. per Node

    59 |
    60 |
    61 |

    {{ stats["min-inst-per-node"] }} / {{ stats["avg-inst-per-node"] }} / {{ stats["max-inst-per-node"] }}

    62 |
    63 |
    64 |
    65 |
    66 |
    67 |
    68 | 69 | 75 |
    76 |
    77 |
    78 |
    79 |
    80 |
    81 |

    Node vs Instance CPUs

    82 |
    83 |
    84 | 85 |
    86 |
    87 |
    88 |
    89 |
    90 |
    91 |

    Disk Total vs. Disk Used

    92 |
    93 |
    94 | 95 |
    96 |
    97 |
    98 |
    99 |
    100 |
    101 |

    Memory Total vs. Memory Used

    102 |
    103 |
    104 | 105 |
    106 |
    107 |
    108 |
    109 | 110 | 200 | {% endblock %} 201 | --------------------------------------------------------------------------------