├── .babelrc ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── ansible ├── hc_wizard.yml ├── hc_wizard_cleanup.yml └── hc_wizard_example_inventory.yml ├── cockpit-gluster.spec ├── contributing.md ├── package-lock.json ├── package.json ├── screenshots ├── README.md ├── dashboard.png ├── volume_modal.png ├── wizard_bricks.png ├── wizard_hosts.png ├── wizard_review.png └── wizard_volumes.png ├── scripts ├── dev_update.py └── rem_install.bash ├── src ├── app.css ├── app.jsx ├── components │ ├── ExpandClusterWizard.jsx │ ├── GlusterManagement.js │ ├── WizardSteps │ │ ├── BrickStep.js │ │ ├── HostStep.js │ │ ├── ReviewStep.js │ │ └── VolumeStep.js │ └── common │ │ ├── Alerts.js │ │ ├── Dropdown.jsx │ │ ├── GeneralWizard.jsx │ │ └── ObjectModal.jsx ├── index.html └── lib │ ├── gluster-ansible.jsx │ ├── util.jsx │ └── validators.jsx ├── static └── manifest.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins":[ 4 | 'transform-class-properties', 5 | 'transform-export-extensions', 6 | 'transform-object-rest-spread', 7 | 'transform-object-assign' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build_dir/ 2 | dist/ 3 | node_modules/ 4 | notes/ 5 | rpm_build/ 6 | VagrantFile 7 | notes/ 8 | .vagrant/ 9 | .bash_aliases 10 | *.swp 11 | .cache-loader/ 12 | geckodriver.log 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TMPREPOS = $(CURDIR)/rpm_build 2 | BUILDIR = $(CURDIR)/build_dir 3 | BUILDROOT = $(CURDIR)/build_root 4 | 5 | PACKAGE = cockpit-gluster 6 | VERSION = 0.0.1 7 | RELEASE = 0 8 | RPMBUILD_ARGS := --define="_topdir $(TMPREPOS)" --define="_version $(VERSION)" --buildroot="$(BUILDROOT)" 9 | 10 | distdir = $(PACKAGE)-$(VERSION) 11 | tarname = $(distdir).tar.gz 12 | 13 | 14 | 15 | tarify : 16 | rm -rf $(TMPREPOS) 17 | mkdir -p $(TMPREPOS)/{SPECS,RPMS,SRPMS,SOURCES} 18 | rm -rf $(BUILDIR) 19 | mkdir -p $(BUILDIR)/$(distdir) 20 | rsync -r dist $(BUILDIR)/$(distdir)/ 21 | rsync -r LICENSE $(BUILDIR)/$(distdir)/dist/ 22 | rsync -r ansible $(BUILDIR)/$(distdir)/ 23 | rsync -r cockpit-gluster.spec $(BUILDIR)/$(distdir)/ 24 | 25 | tar -C $(BUILDIR) -cvzf $(TMPREPOS)/SOURCES/$(tarname) $(distdir) 26 | 27 | 28 | 29 | srpm : tarify 30 | rm -rf $(BUILDROOT) 31 | mkdir -p $(BUILDROOT) 32 | rpmbuild $(RPMBUILD_ARGS) -ts $(TMPREPOS)/SOURCES/$(tarname) 33 | @echo 34 | @echo "srpm available at '$(TMPREPOS)'" 35 | @echo 36 | 37 | rpm : srpm 38 | rpmbuild $(RPMBUILD_ARGS) --rebuild "$(TMPREPOS)"/SRPMS/*.src.rpm 39 | @echo 40 | @echo "rpm(s) available at '$(TMPREPOS)'" 41 | @echo 42 | clean-rpm : 43 | rm -rf $(TMPREPOS) $(BUILDIR) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note: This project is not currently being maintained. 2 | 3 | # cockpit-gluster 4 | A [cockpit](https://github.com/cockpit-project/cockpit) plugin that provides a status view and volume actions for a gluster cluster with [glusterd2](https://github.com/gluster/glusterd2)(GD2) based bricks. 5 | 6 | It runs on any of the brick nodes (where glusterd2 is running). 7 | 8 | ### Contents: 9 | - [Features](#features) 10 | - [Build](#building-from-source) 11 | - [Developing with vagrant](#developing-with-vagrant) (also the quickest way to try out a gd2 cluster locally) 12 | - [Package](#make-an-rpm) 13 | - [Setup GD2 environment](#setup-your-brick-servers-with-gd2) 14 | - [Install](#installing-on-one-of-the-gd2-nodes) 15 | - [Access](#browse-to-the-cockpit-port) 16 | - [Create volumes](#creating-volumes) 17 | - [Screenshots](#screenshots) 18 | 19 | 20 | ### Features: 21 | - Status panel for monitoring peers, volumes and bricks 22 | - A Wizard for brick setup and volume deployment. 23 | ### Pending Features 24 | - GD2 rest auth support. 25 | - Supporting all volume types. 26 | 27 | 28 | 29 | 30 | ## Building from source 31 | 32 | ### Install Build Dependencies 33 | 34 | ``` 35 | sudo yum install -y npm 36 | npm install 37 | ``` 38 | 39 | ### Build the project 40 | ``` 41 | ./node_modules/.bin/webpack 42 | ``` 43 | 44 | ### Developing with vagrant: 45 | After building run these commands in the repo folder: 46 | ``` 47 | #install vagrant 48 | sudo yum install vagrant -y 49 | sudo vagrant up 50 | 51 | ``` 52 | and browse to `localhost:9091`. Congratulations, you're up! 53 | 54 | After making any changes to the code you can run `npx webpack && sudo vagrant rsync` to build and copy the plugin to the VMs. 55 | Refresh the browser to see your changes. 56 | 57 | 58 | ## Make an rpm 59 | ``` 60 | make rpm 61 | ``` 62 | 63 | 64 | ## Setup your brick servers with GD2 65 | 66 | 67 | As GD2 is in development, it is recommended to build it from the `master` branch and deploy it with an external etcd. 68 | See [GD2 Resources](#gd2-resources) for more information, and automated setup. 69 | 70 | ## Installing (on one of the GD2 nodes) 71 | ### Install gluster-ansible and its dependencies: 72 | 73 | gluster-ansible is hosted in a [copr repo](https://copr.fedorainfracloud.org/coprs/sac/gluster-ansible/) 74 | 75 | #### Install the repo: 76 | 77 | | Operating System | Install Command | 78 | | ------------- | --------------- | 79 | | Centos 7 | `sudo curl -o /etc/yum.repos.d/gluster-ansible.repo https://copr.fedorainfracloud.org/coprs/sac/gluster-ansible/repo/epel-7/sac-gluster-ansible-epel-7.repo` | 80 | | Fedora 27+ | `sudo dnf copr enable sac/gluster-ansible` | 81 | 82 | 83 | #### Install the packages from the repo: 84 | 85 | ``` 86 | sudo yum install gluster-ansible python-gluster-mgmt-client 87 | 88 | ``` 89 | 90 | ## Install cockpit-gluster 91 | ``` 92 | yum install -y cockpit-gluster-x.x.x-x.noarch.rpm 93 | ``` 94 | ## Browse to the cockpit port: 95 | `http://your-cockpithost.domain:9090` 96 | 97 | 98 | 99 | ## Creating volumes 100 | 101 | Setup passwordless ssh from the managing host (where cockpit-gluster is installed) to all the other nodes: 102 | 103 | Run these commands on the managing host: 104 | ``` 105 | ssh-keygen 106 | 107 | ssh-copy-id root@host1.example.com 108 | ssh-copy-id root@host2.example.com 109 | ssh-copy-id root@host3.example.com 110 | ``` 111 | 112 | Click on: 113 | - Create Volume (if creating on the existing cluster) 114 | - Expand Cluster (if creating on new nodes) 115 | 116 | ## Screenshots 117 | ### Dashboard 118 | ![Dashboard Image](/screenshots/dashboard.png?raw=true "Dashboard") 119 | 120 | ### Volume Creation Wizard 121 | ![Wizard Hosts Image](/screenshots/wizard_hosts.png?raw=true "Wizard Hosts") 122 | ![Wizard Volumes Image](/screenshots/wizard_volumes.png?raw=true "Wizard Volumes") 123 | ![Wizard Bricks Image](/screenshots/wizard_bricks.png?raw=true "Wizard Bricks") 124 | ![Wizard Review Image](/screenshots/wizard_review.png?raw=true "Wizard Review") 125 | 126 | 127 | 128 | ## GD2 Resources 129 | 130 | GD2 developement guide: https://github.com/gluster/glusterd2/blob/master/doc/development-guide.md 131 | 132 | GD2 quickstart guide: https://github.com/gluster/glusterd2/blob/master/doc/quick-start-user-guide.md 133 | 134 | A two command cluster setup with ansible and bash (for CentOS machines): https://github.com/rohantmp/gd2-testing 135 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # vi: set ft=ruby : 2 | # 3 | 4 | 5 | cluster_count = 1 6 | node_data_disk_count = 2 7 | driveletters = ('a'..'z').to_a 8 | disk_size = 20 #GB 9 | cpus = 2 10 | memory = 2048 11 | 12 | node_count = cluster_count * 3 13 | total_disks = node_count * node_data_disk_count 14 | total_disk_usage = total_disks * disk_size 15 | enable_glusterd2_rest_auth = false 16 | 17 | Vagrant.configure(2) do |config| 18 | puts "Creating #{cluster_count} clusters." 19 | puts "Creating #{node_count} nodes." 20 | puts "Creating #{node_data_disk_count} data disks (#{disk_size}G) each." 21 | puts "Using #{total_disk_usage} GB!." 22 | 23 | (1..node_count).reverse_each do |num| 24 | config.vm.define "node-#{num}" do |node| 25 | vm_ip = "192.168.250.#{num+10}" 26 | 27 | node.vm.box = "centos/7" 28 | node.vm.synced_folder ".", "/vagrant", disabled: true 29 | node.vm.network "private_network", 30 | :ip => vm_ip, 31 | :libvirt__driver_queues => "#{cpus}" 32 | node.vm.post_up_message = "VM private ip: #{vm_ip}" 33 | node.vm.hostname = "gd2-node-#{num}" 34 | 35 | node.vm.provider "libvirt" do |lvt| 36 | lvt.memory = "#{memory}" 37 | lvt.cpus = "#{cpus}" 38 | lvt.nested = false 39 | lvt.cpu_mode = "host-passthrough" 40 | lvt.volume_cache = "writeback" 41 | lvt.graphics_type = "none" 42 | lvt.video_type = "vga" 43 | lvt.video_vram = 1024 44 | # lvt.usb_controller :model => "none" # (requires vagrant-libvirt 0.44 which is not in Fedora yet) 45 | lvt.random :model => 'random' 46 | lvt.channel :type => 'unix', :target_name => 'org.qemu.guest_agent.0', :target_type => 'virtio' 47 | #disk_config 48 | disks = [] 49 | (2..(node_data_disk_count+1)).each do |d| 50 | lvt.storage :file, :device => "vd#{driveletters[d]}", :size => "#{disk_size}G" 51 | disks.push "/dev/vd#{driveletters[d]}" 52 | end 53 | 54 | end 55 | 56 | if num == 1 57 | node.vm.synced_folder "./dist", "/root/.local/share/cockpit/gluster-management", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"] 58 | node.vm.synced_folder "./ansible/", "/etc/ansible//", type: "rsync", create: true, rsync__args: ["--verbose","--archive"] 59 | node.vm.network "forwarded_port", guest: 9090, host: 9091 60 | node.vm.post_up_message << "You can now access Cockpit at http://localhost:9091 (login as 'admin' with password 'foobar')" 61 | end 62 | 63 | node.vm.provision "shell", inline: <<-SHELL 64 | set -u 65 | 66 | 67 | yum update -y --disableplugin=subscription-manager --downloaddir=/dev/shm 68 | yum install -y --disableplugin=subscription-manager --downloaddir=/dev/shm util-linux 69 | 70 | echo foobar | passwd --stdin root 71 | getent passwd admin >/dev/null || useradd -c Administrator -G wheel admin 72 | echo foobar | passwd --stdin admin 73 | 74 | usermod -a -G wheel vagrant 75 | 76 | 77 | 78 | yum install -y --disableplugin=subscription-manager --downloaddir=/dev/shm \ 79 | storaged \ 80 | storaged-lvm2 \ 81 | yum-utils \ 82 | cockpit \ 83 | python-requests \ 84 | python-jwt \ 85 | epel-release 86 | # ovirt-hosted-engine-setup 87 | curl -o /etc/yum.repos.d/gd2-master.repo http://artifacts.ci.centos.org/gluster/gd2-nightly/gd2-master.repo 88 | curl -o /etc/yum.repos.d/sac-gluster-ansible-epel-7.repo https://copr.fedorainfracloud.org/coprs/sac/gluster-ansible/repo/epel-7/sac-gluster-ansible-epel-7.repo 89 | curl -o /etc/yum.repos.d/glusterfs-nightly-master.repo http://artifacts.ci.centos.org/gluster/nightly/master.repo 90 | yum install -y --disableplugin=subscription-manager --downloaddir=/dev/shm \ 91 | glusterd2 \ 92 | gluster-ansible python-gluster-mgmt-client \ 93 | glusterfs-server glusterfs-fuse glusterfs-api 94 | 95 | ansible localhost -m lineinfile -a "path=/etc/glusterd2/glusterd2.toml state=present regexp='^[^#]*restauth\s*=\s*' line=restauth=#{enable_glusterd2_rest_auth}" 96 | ansible localhost -m lineinfile -a "path=/etc/glusterd2/glusterd2.toml state=present regexp='^[^#]*peeraddress\s*=\s*' line=peeraddress='#{vm_ip}:24008'" 97 | ansible localhost -m lineinfile -a "path=/etc/ssh/sshd_config state=present regexp='^[#]*PasswordAuthentication\s+yes' line='PasswordAuthentication yes'" 98 | systemctl restart sshd 99 | 100 | systemctl enable --now cockpit.socket 101 | systemctl enable --now glusterd2 102 | printf "[WebService]\nAllowUnencrypted=true\n" > /etc/cockpit/cockpit.conf 103 | systemctl daemon-reload 104 | systemctl restart sshd 105 | if [[ ! -f ~/.ssh/id_rsa ]] 106 | then 107 | ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa 108 | fi 109 | 110 | rm -rf /dev/shm/yumdb 111 | 112 | if [[ #{num} -eq 1 ]] 113 | then 114 | for i in {10..#{node_count+10}} 115 | do 116 | echo Setting passwordless ssh to root on 192.168.250.${i} 117 | sshpass -p foobar ssh-copy-id root@192.168.250.${i} -o StrictHostKeyChecking=no 118 | done 119 | fi 120 | SHELL 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /ansible/hc_wizard.yml: -------------------------------------------------------------------------------- 1 | - name: Setup Bricks 2 | hosts: hc_nodes 3 | remote_user: root 4 | gather_facts: yes 5 | 6 | vars: 7 | gluster_features_hci_packages: [] 8 | gluster_features_hci_cluster: "{{ groups['hc_nodes'] }}" 9 | gluster_infra_lv_thinpoolname: gluster_thinpool 10 | pre_tasks: 11 | - name: Install ansible dependencies 12 | package: 13 | name: "{{ item }}" 14 | state: present 15 | with_items: 16 | - python-jwt 17 | - python-requests 18 | - policycoreutils-python 19 | roles: 20 | - gluster.infra 21 | tags: 22 | - wizard_bricks 23 | 24 | 25 | - name: Deploy HCI 26 | hosts: local 27 | vars: 28 | 29 | gluster_features_hci_master: localhost 30 | gluster_features_hci_cluster: "{{ groups['hc_nodes'] }}" 31 | roles: 32 | - gluster.features 33 | tags: 34 | - wizard_volumes 35 | -------------------------------------------------------------------------------- /ansible/hc_wizard_cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Warning: This playbook assumes that the information in the inventory is correct 3 | # and performs naive shell commands using those values as inputs. 4 | # Use only if you are sure that the values in the inventory don't coincide with 5 | # anything that you don't want cleaned up. 6 | 7 | - hosts: local 8 | tasks: 9 | - name: Delete the volumes 10 | shell: glustercli volume stop {{ item.volname }};glustercli volume delete {{ item.volname }} -y 11 | register: shell_output 12 | changed_when: shell_output.rc == 0 13 | #failed_when False 14 | with_items: 15 | - "{{ gluster_features_hci_volumes }}" 16 | tags: 17 | - delete_volumes 18 | - name: Remove the peers 19 | shell: glustercli peer remove $(glustercli peer list|grep {{ item }}|awk '{ print $2; }') 20 | register: shell_output 21 | changed_when: shell_output.rc == 0 22 | #failed_when False 23 | with_inventory_hostnames: 24 | - hc_nodes 25 | tags: 26 | - remove_peers 27 | - name: Remove brick setup 28 | hosts: hc_nodes 29 | tasks: 30 | - name: Unmount and delete bricks 31 | shell: umount {{ item.path }};e=$?; rm -rf {{ item.path }};exit $e 32 | register: shell_output 33 | changed_when: shell_output.rc == 0 34 | failed_when: False 35 | with_items: 36 | - "{{ gluster_infra_mount_devices }}" 37 | 38 | - name: Wipe filesystem from LVs 39 | shell: wipefs -a /dev/{{ item.vgname }}/{{ item.lvname}} 40 | register: shell_output 41 | changed_when: shell_output.rc == 0 42 | #failed_when False 43 | with_items: 44 | - "{{ gluster_infra_mount_devices }}" 45 | 46 | - name: Remove VG 47 | shell: vgremove {{ item.vgname }} -ff 48 | register: shell_output 49 | changed_when: shell_output.rc == 0 50 | with_items: "{{ gluster_infra_volume_groups }}" 51 | when: gluster_infra_volume_groups is defined 52 | #failed_when False 53 | - name: Remove VDO 54 | shell: vdo remove -n {{ item.name }} 55 | register: shell_output 56 | changed_when: shell_output.rc == 0 57 | #failed_when False 58 | with_items: 59 | - "{{ gluster_infra_vdo }}" 60 | when: gluster_infra_vdo is defined 61 | tags: 62 | - cleanup_bricks 63 | -------------------------------------------------------------------------------- /ansible/hc_wizard_example_inventory.yml: -------------------------------------------------------------------------------- 1 | hc_nodes: 2 | hosts: 3 | host1.example.com: 4 | gluster_infra_volume_groups: 5 | - vgname: vg_sdb 6 | pvname: /dev/sdb 7 | - vgname: vg_sdc 8 | pvname: /dev/sdc 9 | - vgname: vg_sdd 10 | pvname: /dev/sdd 11 | gluster_infra_mount_devices: 12 | - path: /gluster_bricks/engine 13 | lvname: gluster_lv_engine 14 | vgname: vg_sdb 15 | - path: /gluster_bricks/data 16 | lvname: gluster_lv_data 17 | vgname: vg_sdc 18 | - path: /gluster_bricks/vmstore 19 | lvname: gluster_lv_vmstore 20 | vgname: vg_sdd 21 | gluster_infra_cache_vars: 22 | - vgname: vg_sdb 23 | cachedisk: /dev/sde 24 | cachelvname: cachelv_thinpool_vg_sdb 25 | cachethinpoolname: thinpool_vg_sdb 26 | cachelvsize: 18G 27 | cachemetalvsize: 2G 28 | cachemetalvname: cache_thinpool_vg_sdb 29 | cachemode: writethrough 30 | gluster_infra_thick_lvs: 31 | - vgname: vg_sdb 32 | lvname: gluster_lv_engine 33 | size: 100G 34 | gluster_infra_thinpools: 35 | - vgname: vg_sdc 36 | thinpoolname: thinpool_vg_sdc 37 | - vgname: vg_sdd 38 | thinpoolname: thinpool_vg_sdd 39 | gluster_infra_lv_logicalvols: 40 | - vgname: vg_sdc 41 | thinpool: thinpool_vg_sdc 42 | lvname: gluster_lv_data 43 | lvsize: 100G 44 | - vgname: vg_sdd 45 | thinpool: thinpool_vg_sdd 46 | lvname: gluster_lv_vmstore 47 | lvsize: 100G 48 | host2.example.com: 49 | gluster_infra_volume_groups: 50 | - vgname: vg_sdb 51 | pvname: /dev/sdb 52 | - vgname: vg_sdc 53 | pvname: /dev/sdc 54 | - vgname: vg_sdd 55 | pvname: /dev/sdd 56 | gluster_infra_mount_devices: 57 | - path: /gluster_bricks/engine 58 | lvname: gluster_lv_engine 59 | vgname: vg_sdb 60 | - path: /gluster_bricks/data 61 | lvname: gluster_lv_data 62 | vgname: vg_sdc 63 | - path: /gluster_bricks/vmstore 64 | lvname: gluster_lv_vmstore 65 | vgname: vg_sdd 66 | gluster_infra_cache_vars: 67 | - vgname: vg_sdb 68 | cachedisk: /dev/sde 69 | cachelvname: cachelv_thinpool_vg_sdb 70 | cachethinpoolname: thinpool_vg_sdb 71 | cachelvsize: 18G 72 | cachemetalvsize: 2G 73 | cachemetalvname: cache_thinpool_vg_sdb 74 | cachemode: writethrough 75 | gluster_infra_thick_lvs: 76 | - vgname: vg_sdb 77 | lvname: gluster_lv_engine 78 | size: 100G 79 | gluster_infra_thinpools: 80 | - vgname: vg_sdc 81 | thinpoolname: thinpool_vg_sdc 82 | - vgname: vg_sdd 83 | thinpoolname: thinpool_vg_sdd 84 | gluster_infra_lv_logicalvols: 85 | - vgname: vg_sdc 86 | thinpool: thinpool_vg_sdc 87 | lvname: gluster_lv_data 88 | lvsize: 100G 89 | - vgname: vg_sdd 90 | thinpool: thinpool_vg_sdd 91 | lvname: gluster_lv_vmstore 92 | lvsize: 100G 93 | host3.example.com: 94 | gluster_infra_volume_groups: 95 | - vgname: vg_sdb 96 | pvname: /dev/sdb 97 | - vgname: vg_sdc 98 | pvname: /dev/sdc 99 | - vgname: vg_sdd 100 | pvname: /dev/sdd 101 | gluster_infra_mount_devices: 102 | - path: /gluster_bricks/engine 103 | lvname: gluster_lv_engine 104 | vgname: vg_sdb 105 | - path: /gluster_bricks/data 106 | lvname: gluster_lv_data 107 | vgname: vg_sdc 108 | - path: /gluster_bricks/vmstore 109 | lvname: gluster_lv_vmstore 110 | vgname: vg_sdd 111 | gluster_infra_cache_vars: 112 | - vgname: vg_sdb 113 | cachedisk: /dev/sde 114 | cachelvname: cachelv_thinpool_vg_sdb 115 | cachethinpoolname: thinpool_vg_sdb 116 | cachelvsize: 18G 117 | cachemetalvsize: 2G 118 | cachemetalvname: cache_thinpool_vg_sdb 119 | cachemode: writethrough 120 | gluster_infra_thick_lvs: 121 | - vgname: vg_sdb 122 | lvname: gluster_lv_engine 123 | size: 100G 124 | gluster_infra_thinpools: 125 | - vgname: vg_sdc 126 | thinpoolname: thinpool_vg_sdc 127 | - vgname: vg_sdd 128 | thinpoolname: thinpool_vg_sdd 129 | gluster_infra_lv_logicalvols: 130 | - vgname: vg_sdc 131 | thinpool: thinpool_vg_sdc 132 | lvname: gluster_lv_data 133 | lvsize: 100G 134 | - vgname: vg_sdd 135 | thinpool: thinpool_vg_sdd 136 | lvname: gluster_lv_vmstore 137 | lvsize: 100G 138 | vars: 139 | gluster_infra_stripe_unit_size: 256 140 | gluster_infra_disktype: JBOD 141 | gluster_infra_diskcount: 12 142 | ansible_ssh_common_args: '-o StrictHostKeyChecking=no' 143 | local: 144 | hosts: 145 | localhost: null 146 | vars: 147 | gluster_features_hci_volumes: 148 | - volname: engine 149 | brick: /gluster_bricks/engine/engine 150 | arbiter: false 151 | master: localhost 152 | - volname: data 153 | brick: /gluster_bricks/data/data 154 | arbiter: false 155 | master: localhost 156 | - volname: vmstore 157 | brick: /gluster_bricks/vmstore/vmstore 158 | arbiter: false 159 | master: localhost 160 | ansible_ssh_common_args: '-o StrictHostKeyChecking=no' 161 | -------------------------------------------------------------------------------- /cockpit-gluster.spec: -------------------------------------------------------------------------------- 1 | %global _plugindir %{_datarootdir}/cockpit 2 | 3 | Name: cockpit-gluster 4 | Version: %{_version} 5 | Release: 1 6 | Summary: A Cockpit plugin to deploy and manage Gluster 7 | 8 | License: ASL 2.0 9 | URL: https://example.com/%{name} 10 | Source0: https://example.com/%{name}/release/%{name}-%{version}.tar.gz 11 | 12 | BuildArch: noarch 13 | 14 | Requires: cockpit 15 | Requires: ansible 16 | 17 | Prefix: %{_plugindir} 18 | %description 19 | A Cockpit plugin to deploy and manage Gluster 20 | 21 | 22 | %prep 23 | %setup -q 24 | 25 | %install 26 | mkdir -p %{buildroot}/%{_plugindir}/gluster-management/ansible/ 27 | install -m 644 dist/* -t %{buildroot}/%{_plugindir}/gluster-management/ 28 | 29 | mkdir -p %{buildroot}/etc/ansible/ 30 | 31 | install -m 644 "ansible/hc_wizard.yml" -t %{buildroot}/etc/ansible/ 32 | install -m 644 "ansible/hc_wizard_cleanup.yml" -t %{buildroot}/etc/ansible/ 33 | install -m 644 "ansible/hc_wizard_example_inventory.yml" -t %{buildroot}/etc/ansible/ 34 | 35 | 36 | %files 37 | %{_plugindir}/gluster-management 38 | /etc/ansible/hc_wizard.yml 39 | /etc/ansible/hc_wizard_cleanup.yml 40 | /etc/ansible/hc_wizard_example_inventory.yml 41 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Code Guidelines 2 | Please raise an issue if there is a problem with one of these: 3 | 4 | - Follow React Patterns 5 | - Never modify state directly, use setState(newState) or setState(callback(prevState)=>newState) instead. 6 | - Every reference to state outside of the render() and constructor(props) should be inside a setState callback. 7 | - Do not modify props variables from inside the receiving component. 8 | - Try to create PropTypes entries for your components 9 | - If the behaviour your code is not obvious, leave a comment 10 | - Do not initialize state with props unless you hard copy them. 11 | 12 | ## Variable naming 13 | - lowerCamelCase for variables, properties, etc. 14 | - UpperCamelCase for constructors. 15 | - CAPITAL_CASE_WITH_UNDERSCORES for constants. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cockpit-gluster", 3 | "version": "1.0.0", 4 | "description": "Cockpit plugin that provides management dashboard for Gluster.", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "test": "echo Go write some tests sucka!" 8 | }, 9 | "author": "Rohan CJ", 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "babel-core": "^6.26.3", 13 | "babel-loader": "^7.1.5", 14 | "babel-plugin-transform-class-properties": "^6.24.1", 15 | "babel-plugin-transform-export-extensions": "^6.22.0", 16 | "babel-plugin-transform-object-assign": "^6.22.0", 17 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 18 | "babel-preset-env": "^1.7.0", 19 | "babel-preset-react": "^6.24.1", 20 | "cache-loader": "^1.2.2", 21 | "clean-webpack-plugin": "^0.1.19", 22 | "copy-webpack-plugin": "^4.5.1", 23 | "css-loader": "^0.28.11", 24 | "html-webpack-plugin": "^3.2.0", 25 | "style-loader": "^0.21.0", 26 | "webpack": "^4.11.0", 27 | "webpack-cli": "^3.0.2" 28 | }, 29 | "dependencies": { 30 | "classnames": "^2.2.3", 31 | "js-yaml": "^3.12.0", 32 | "jsonwebtoken": "^8.3.0", 33 | "patternfly-react": "^2.11.1", 34 | "react": "^16.4.1", 35 | "react-dom": "^16.4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /screenshots/README.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | ## Dashboard 3 | ![Dashboard Image](dashboard.png?raw=true "Dashboard") 4 | ![Volume Modal Image](volume_modal.png?raw=true "Volume Modal") 5 | ## Wizard 6 | ![Wizard Hosts Image](wizard_hosts.png?raw=true "Wizard Hosts") 7 | ![Wizard Volumes Image](wizard_volumes.png?raw=true "Wizard Volumes") 8 | ![Wizard Bricks Image](wizard_bricks.png?raw=true "Wizard Bricks") 9 | ![Wizard Review Image](wizard_review.png?raw=true "Wizard Review") 10 | -------------------------------------------------------------------------------- /screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/dashboard.png -------------------------------------------------------------------------------- /screenshots/volume_modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/volume_modal.png -------------------------------------------------------------------------------- /screenshots/wizard_bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/wizard_bricks.png -------------------------------------------------------------------------------- /screenshots/wizard_hosts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/wizard_hosts.png -------------------------------------------------------------------------------- /screenshots/wizard_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/wizard_review.png -------------------------------------------------------------------------------- /screenshots/wizard_volumes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gluster/cockpit-gluster/d0f0f68a723058e18d5dd4467db05905188508e8/screenshots/wizard_volumes.png -------------------------------------------------------------------------------- /scripts/dev_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import sys 4 | import pyinotify 5 | import subprocess 6 | from selenium import webdriver 7 | 8 | 9 | 10 | cur_dir = os.path.dirname(os.path.realpath(__file__)) 11 | src_dir = os.path.abspath(os.path.join(cur_dir, os.pardir,'src')) 12 | 13 | def src_watch(on_change, watch_dir="src/", browser=None): 14 | wm = pyinotify.WatchManager() # Watch Manager 15 | mask = pyinotify.IN_MODIFY # watched events 16 | 17 | class EventHandler(pyinotify.ProcessEvent): 18 | def process_IN_MODIFY(self, event): 19 | on_change() 20 | try_count = 0 21 | keep_trying = True 22 | max_tries = 1000 23 | if browser: 24 | try: 25 | browser.refresh() 26 | print("[Browser Refreshed]") 27 | except BrokenPipeError: 28 | print("[Could not refresh browser: BrokenPipeError]") 29 | print("Try a different version of your browser's driver!") 30 | # while keep_trying: 31 | # try: 32 | # try_count += 1 33 | # browser.refresh() 34 | # print("Browser refreshed]") 35 | # except BrokenPipeError: 36 | # if try_count > max_tries: 37 | # keep_trying = False 38 | 39 | handler = EventHandler() 40 | notifier = pyinotify.Notifier(wm, handler) 41 | wdd = wm.add_watch(watch_dir, mask, rec=True) 42 | try: 43 | print("[Watching %s]" % watch_dir) 44 | notifier.loop() 45 | finally: 46 | wm.rm_watch(wdd.values()) 47 | print("[Stopped watching]") 48 | 49 | def start_browser(url=None): 50 | driver = webdriver.Firefox() 51 | if url: 52 | driver.get(url) 53 | return driver 54 | 55 | def on_change(): 56 | global cur_dir 57 | print("[Watch triggered]") 58 | script_path = os.path.abspath(os.path.join(cur_dir, 'rem_install.bash')) 59 | user = 'root' 60 | if len(sys.argv) > 2: 61 | user = sys.argv[2] 62 | remote_host = 'root@%s' % (sys.argv[1]) 63 | subprocess.call([script_path,remote_host,user]) 64 | 65 | def pause_and_prompt(): 66 | try: 67 | print("[Paused]\nPress Enter to resume or Ctrl-c again to exit") 68 | ret = input() 69 | return True 70 | except KeyboardInterrupt: 71 | return False 72 | 73 | def main(): 74 | global src_dir 75 | if len(sys.argv) < 2: 76 | print("Please provide host") 77 | keep_watching = True 78 | host = sys.argv[1] 79 | browser = start_browser(url="http://%s:9090" % host) 80 | while keep_watching: 81 | src_watch(on_change, src_dir, browser=browser) 82 | keep_watching = pause_and_prompt() 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /scripts/rem_install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | help="Compiles, Packages, Pushes to Server via root ssh. Installs.\nUsage: rem_install.bash []" 4 | if [[ ${1} = "help" || ${1} = "--help"|| ${1} = "-h" ]] 5 | then 6 | echo -e ${help} 7 | return 0 8 | fi 9 | 10 | 11 | if [[ $# -eq 1 ]] 12 | then 13 | export HOST=$1 14 | export REMOTE_USER="root" 15 | elif [[ $# -eq 2 ]] 16 | then 17 | export HOST=$1 18 | export REMOTE_USER=$2 19 | else 20 | echo -e $help 21 | exit 1 22 | fi 23 | 24 | if [[ $REMOTE_USER = "root" ]] 25 | then 26 | export USERHOME="/root/" 27 | else 28 | export USERHOME="/home/${REMOTE_USER}/" 29 | fi 30 | 31 | 32 | npx webpack && 33 | make rpm && 34 | rsync rpm_build/RPMS/noarch/cockpit-gluster*.rpm \ 35 | ${HOST}:${USERHOME}/cockpit-gluster.rpm && 36 | ssh $HOST \ 37 | "rpm -evh cockpit-gluster; \ 38 | rpm -ivh ${USERHOME}/cockpit-gluster.rpm \ 39 | --relocate /usr/share/cockpit=${USERHOME}/.local/share/cockpit" 40 | notify-send "done" 41 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | div.gluster-management .table > thead > tr > th, 2 | div.gluster-management .table > tbody > tr > th, 3 | div.gluster-management .table > tfoot > tr > th, 4 | div.gluster-management .table > thead > tr > td, 5 | div.gluster-management .table > tbody > tr > td, 6 | div.gluster-management .table > tfoot > tr > td 7 | { 8 | padding:8px; 9 | } 10 | 11 | .gluster-management div.volume-bricks-table{ 12 | margin: 8px 0px; 13 | } 14 | 15 | 16 | div.gluster-management .table > thead{ 17 | background-image: none; 18 | background-color: inherit; 19 | } 20 | 21 | 22 | .container-fluid.gluster-management{ 23 | margin-left: 4%; 24 | margin-right: 2%; 25 | } 26 | 27 | .gluster-management .panel-heading{ 28 | font-size: 1.2em; 29 | } 30 | 31 | 32 | .table-hover.gluster-management > tbody > tr.no-highlight:hover > td, .table-hover > tbody > tr.no-highlight:hover > td, 33 | .table-hover.gluster-management > tbody > tr.no-highlight:hover > td, .table-hover > tbody > tr.no-highlight:hover > th 34 | .table-hover.gluster-management > tbody > tr.no-highlight > td, .table-hover > tbody > tr.no-highlight > td, 35 | .table-hover.gluster-management > tbody > tr.no-highlight > td, .table-hover > tbody > tr.no-highlight > th 36 | { 37 | background-color: #f5f5f5; 38 | cursor: default; 39 | } 40 | 41 | th.volume-expando, td.volume-expando{ 42 | box-sizing: content-box; 43 | font-size: 20px; 44 | width: 20px; 45 | } 46 | 47 | .gluster-management .fa { 48 | display: inline; 49 | } 50 | 51 | button.refresh-btn{ 52 | margin-left: 10px; 53 | } 54 | 55 | .object-modal td, .object-modal th{ 56 | padding:8px; 57 | } 58 | 59 | .object-modal-btn .start-modal-btn .stop-modal-btn .delete-modal-btn { 60 | background-color:transparent; 61 | padding-left: 7px; 62 | padding-right:7px; 63 | } 64 | 65 | .object-modal-btn { 66 | color: #00659c; 67 | } 68 | 69 | .start-modal-btn { 70 | color: green; 71 | } 72 | 73 | .stop-modal-btn { 74 | color: red; 75 | } 76 | 77 | .delete-modal-btn { 78 | color: black; 79 | } 80 | 81 | .object-modal-btn:hover{ 82 | text-decoration: none; 83 | } 84 | 85 | .gluster-management span.status-icon{ 86 | font-size:20px; 87 | padding-right: 8px; 88 | } 89 | 90 | .gluster-management span.fa-arrow-circle-o-up.status-icon{ 91 | color:green; 92 | } 93 | 94 | .gluster-management span.fa-arrow-circle-o-down.status-icon{ 95 | color:red; 96 | } 97 | 98 | .gluster-management .title{ 99 | margin-bottom: 27px; 100 | } 101 | 102 | div.wizard-step-container { 103 | padding: 1% 5%; 104 | } 105 | 106 | div.brick-col { 107 | padding-left: 0px; 108 | padding-right:0px; 109 | } 110 | 111 | .wizard-checkbox input[type=checkbox]{ 112 | height: 25px; 113 | width: 16px; 114 | margin-bottom: 0px; 115 | margin-left: -10px; 116 | } 117 | div.wizard-checkbox.thinpool{ 118 | width: 6%; 119 | } 120 | div.wizard-checkbox.vdo input[type=checkbox]{ 121 | margin-left: 20%; 122 | } 123 | .checkbox.wizard-checkbox{ 124 | margin-top: 0px; 125 | } 126 | 127 | div.brick-form-group{ 128 | margin-bottom: 0px; 129 | } 130 | 131 | .brick-title-row, .brick-entry-row{ 132 | margin-left:8%; 133 | } 134 | 135 | div.wizard-brick-rows{ 136 | margin-left:2.6%; 137 | } 138 | div.cache-config-rows { 139 | margin-left:8.0%; 140 | } 141 | 142 | .cache-label { 143 | margin-left: 10px; 144 | } 145 | 146 | .brick-entry-row{ 147 | margin-bottom:0px; 148 | } 149 | 150 | textarea.wizard-preview{ 151 | width: 100%; 152 | height: 270px; 153 | resize: vertical; 154 | } 155 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import GlusterManagement from './components/GlusterManagement' 4 | ReactDOM.render( 5 | , 6 | document.getElementById('app') 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/ExpandClusterWizard.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import GeneralWizard from './common/GeneralWizard' 4 | import HostStep from './WizardSteps/HostStep' 5 | import VolumeStep from './WizardSteps/VolumeStep' 6 | import BrickStep from './WizardSteps/BrickStep' 7 | import ReviewStep from './WizardSteps/ReviewStep' 8 | 9 | import { defaultGlusterModel, runGlusterAnsible, INVENTORY } from '../lib/gluster-ansible' 10 | 11 | 12 | 13 | class ExpandClusterWizard extends Component { 14 | constructor(props){ 15 | super(props) 16 | //TODO: push glusterModel out to seperate file and handle different defaults 17 | this.state = { 18 | glusterModel: JSON.parse(JSON.stringify(defaultGlusterModel)), 19 | volumeStepValid:true, 20 | show: true, 21 | loading: false, 22 | isBackDisabled: false, 23 | isNextDisabled: false, 24 | showValidation: false, 25 | activeStepIndex: 0, 26 | isDeploymentStarted: false, 27 | deploymentPromise: null, 28 | deploymentStream: "", 29 | deploymentState: "", 30 | isRetry: false 31 | 32 | } 33 | if (this.props.type == "createVolume"){ 34 | this.title = "Create Volume"; 35 | console.debug("EC.createVolume gM.hosts",this.state.glusterModel.hosts); 36 | this.state.glusterModel.hosts = this.props.peers.slice(0,3).map((host)=>host.name); 37 | this.state.glusterModel.volumes = [{ 38 | name: "", 39 | type: "replicate", 40 | isArbiter: false, 41 | brickDir: "" 42 | }]; 43 | } 44 | else{ 45 | this.title = "Expand Cluster"; 46 | console.debug("EC.expandCluster gM.hosts",this.state.glusterModel.hosts); 47 | this.state.glusterModel.volumes = [{ 48 | name: "engine", 49 | type: "replicate", 50 | isArbiter: false, 51 | brickDir: "/gluster_bricks/engine/engine" 52 | }, 53 | { 54 | name: "data", 55 | type: "replicate", 56 | isArbiter: false, 57 | brickDir: "/gluster_bricks/data/data" 58 | }]; 59 | } 60 | this.defaultCacheMode = { 61 | cache: false, 62 | ssd: "/dev/sdc", 63 | size: 20, 64 | mode: "writethrough" 65 | } 66 | this.getDefaultBrickValue = { 67 | volName: (volume) => {return volume.name}, 68 | device: (volume) => "/dev/sdb", 69 | size: (volume) => 100, 70 | //HACK: waiting for multiple thicklv support from gluster-ansible 71 | //TODO: honor actual thicklv requests 72 | //WARNING: For now, the first vol is always thick. Regardless of this value 73 | thinPool: (volume) => !(volume.name.indexOf('engine') > -1), 74 | mountPoint: (volume) => { 75 | let mountPointSplit = volume.brickDir.split('/'); 76 | mountPointSplit.pop(); 77 | let mountPoint = mountPointSplit.join('/') 78 | return mountPoint 79 | }, 80 | vdo: (volume) => false, 81 | vdoSize: (volume) => 200 82 | } 83 | } 84 | 85 | close = () => { 86 | this.setState({ show: false}); 87 | } 88 | open = () => { 89 | this.setState({ show: true}); 90 | } 91 | exit = () =>{ 92 | //TODO: add confirmation 93 | this.close(); 94 | } 95 | toggle = () => { 96 | this.setState((prevState)=>{ 97 | return { show: !prevState.show } 98 | }); 99 | } 100 | show = () => { 101 | this.setState((prevState)=>{ 102 | return { show: true } 103 | }); 104 | } 105 | handleStepChange = (index) => { 106 | this.setState((prevState)=>{ 107 | let newState = { activeStepIndex: index} 108 | // handle exits 109 | //TODO: handleTransition 110 | this.handleExit(prevState.activeStepIndex,index); 111 | if((this.state.isNextDisabled || this.state.isBackDisabled)){ 112 | newState.activeStepIndex = prevState.activeStepIndex; 113 | newState.showValidation = true; 114 | } 115 | return newState; 116 | }); 117 | } 118 | deploymentStreamer = (data) => { 119 | console.debug("EC.dS.stream", data) 120 | this.setState((prevState)=>{ 121 | return { deploymentStream: prevState.deploymentStream+data} 122 | }); 123 | } 124 | deploymentDone = (data,msg) => { 125 | console.debug("EC.dS.done", data,msg) 126 | this.setState((prevState)=>{ 127 | return { isDeploymentStarted: false, deploymentStream: prevState.deploymentStream+data, deploymentState: "done"} 128 | }); 129 | }; 130 | deploymentFail = (ex,data) => { 131 | console.debug("EC.dS.fail", data,ex) 132 | this.setState((prevState)=>{ 133 | return { isDeploymentStarted: false, deploymentStream: prevState.deploymentStream+data, deploymentState: "failed"} 134 | }); 135 | } 136 | deploy = (event) => { 137 | this.setState((prevState)=>{ 138 | if (!prevState.isDeploymentStarted){ 139 | let depPromise = runGlusterAnsible( 140 | INVENTORY, 141 | this.deploymentStreamer, 142 | this.deploymentDone, 143 | this.deploymentFail 144 | ); 145 | return { isDeploymentStarted: true, deploymentPromise: depPromise, deploymentState: "started", isRetry: false} 146 | } 147 | }); 148 | } 149 | stopDeployment = (e) =>{ 150 | this.setState((prevState)=>{ 151 | if (prevState.isDeploymentStarted){ 152 | this.state.deploymentPromise.close(); 153 | return { isDeploymentStarted: false } 154 | } 155 | }) 156 | } 157 | onCancel = (e) =>{ 158 | this.setState((prevState)=>{ 159 | if (prevState.isDeploymentStarted){ 160 | this.state.deploymentPromise.close(); 161 | return { isDeploymentStarted: false } 162 | } 163 | }) 164 | this.props.onCancel(); 165 | } 166 | 167 | onBack = (e) => { 168 | this.handleStepChange(this.state.activeStepIndex-1) 169 | } 170 | onNext = (e) => { 171 | //console.debug("Next"); 172 | this.handleStepChange(this.state.activeStepIndex+1) 173 | } 174 | handleHostStep = ({hosts, isValid}) => { 175 | ////console.debug("EC.hostChanged,hosts,isValid:",hosts,isValid) 176 | this.setState((prevState)=>{ 177 | let newState = {}; 178 | if (hosts){ 179 | newState.glusterModel = prevState.glusterModel; 180 | newState.glusterModel.hosts = hosts; 181 | } 182 | newState.isNextDisabled = !isValid; 183 | newState.volumeStepValid = isValid; 184 | return newState 185 | }); 186 | } 187 | 188 | handleVolumeStep = ({volumes, isValid}) => { 189 | //console.debug("handleVolumeStep"); 190 | this.setState((prevState)=>{ 191 | let newState = {}; 192 | if (volumes){ 193 | newState.glusterModel = prevState.glusterModel; 194 | newState.glusterModel.volumes = volumes; 195 | } 196 | return newState 197 | }); 198 | } 199 | handleBrickStep = ({raidConfig, bricks, cacheConfig, isValid}) => { 200 | //console.debug("EC.handleBrickStep:"); 201 | this.setState((prevState)=>{ 202 | let newState = {}; 203 | if (bricks){ 204 | //console.debug("^ bricks:", bricks) 205 | newState.glusterModel = prevState.glusterModel; 206 | newState.glusterModel.bricks = bricks; 207 | } 208 | if (raidConfig){ 209 | //console.debug("EC.handleBrickStep.raidConfig",raidConfig); 210 | newState.glusterModel = prevState.glusterModel; 211 | newState.glusterModel.raidConfig = raidConfig; 212 | } 213 | if (cacheConfig){ 214 | //console.debug("EC.handleBrickStep.cacheConfig",cacheConfig); 215 | newState.glusterModel = prevState.glusterModel; 216 | newState.glusterModel.cacheConfig = cacheConfig; 217 | } 218 | return newState 219 | }); 220 | } 221 | handleExit = (prevIndex,index) => { 222 | switch (prevIndex){ 223 | case 0: 224 | this.hostExit(); 225 | break; 226 | case 1: 227 | this.volumeExit(); 228 | break; 229 | // case 2: 230 | // this.bricksExit(); 231 | // break; 232 | // case 3: 233 | // this.reviewExit(); 234 | // break; 235 | } 236 | } 237 | hostExit = () => { 238 | console.debug("EC.hostExit") 239 | this.setState((prevState)=>{ 240 | let newState ={} 241 | newState.glusterModel = prevState.glusterModel; 242 | while(newState.glusterModel.cacheConfig.length < prevState.glusterModel.hosts.length){ 243 | newState.glusterModel.cacheConfig.push(Object.assign({},this.defaultCacheMode)); 244 | } 245 | console.debug("EC.hostExit cacheConfig",newState.glusterModel.cacheConfig) 246 | return newState; 247 | }); 248 | } 249 | volumeExit = () => { 250 | console.debug("EC.volumeExit") 251 | this.setState((prevState)=>{ 252 | let newState = {}; 253 | let hosts = prevState.glusterModel.hosts; 254 | let volumes = prevState.glusterModel.volumes; 255 | newState.glusterModel = this.state.glusterModel; 256 | newState.glusterModel.bricks = [] 257 | while (newState.glusterModel.bricks.length < hosts.length){ 258 | let hostBricks = []; 259 | for(let volumeIndex = 0; volumeIndex < volumes.length;volumeIndex++){ 260 | let volumeBrick = {}; 261 | for (let key in this.getDefaultBrickValue){ 262 | volumeBrick[key] = this.getDefaultBrickValue[key](volumes[volumeIndex]); 263 | } 264 | hostBricks.push(volumeBrick); 265 | } 266 | newState.glusterModel.bricks.push(hostBricks); 267 | } 268 | console.debug("EC.volumeExit bricks",newState.glusterModel.bricks) 269 | return newState; 270 | }); 271 | } 272 | 273 | handleRetry = (event) => { 274 | this.setState({isRetry: true}); 275 | } 276 | 277 | handleCancel = (event) => { 278 | this.props.onCancel(); 279 | } 280 | 281 | render(){ 282 | // console.debug("EC.render",JSON.stringify(this.state.glusterModel)); 283 | let closeMethod = this.close; 284 | let showRetry = this.state.deploymentState == "failed" && !this.state.isRetry; 285 | let finalMethod = showRetry ? this.handleRetry : this.deploy; 286 | let isNextDisabled = this.state.isDeploymentStarted; 287 | let finalText = showRetry ? "Retry" : "Deploy"; 288 | if(this.state.deploymentState == "done"){ 289 | finalMethod = this.handleCancel; 290 | closeMethod = this.handleCancel; 291 | finalText = "Done" 292 | } 293 | return ( 294 | 307 | 313 | 319 | 325 | 334 | 335 | 336 | ); 337 | } 338 | } 339 | 340 | 341 | export default ExpandClusterWizard; 342 | -------------------------------------------------------------------------------- /src/components/GlusterManagement.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import jwt from 'jsonwebtoken' 3 | import ExpandClusterWizard from './ExpandClusterWizard' 4 | import { ObjectModal, ObjectModalButton, StartModalButton, StopModalButton, DeleteModalButton } from './common/ObjectModal' 5 | import { InlineAlert } from './common/Alerts' 6 | 7 | class GlusterManagement extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | selectedVolumes: {}, 12 | peers: null, 13 | volumeBricks: null, 14 | volumes: null, 15 | expandClusterStarted: false, 16 | showExpandCluster: false, 17 | createVolumeStarted: false, 18 | showCreateVolume: false 19 | }; 20 | this.gluster_api = cockpit.http("24007"); 21 | this.expandClusterWizard = React.createRef(); 22 | this.createVolumeWizard = React.createRef(); 23 | //Binding "this" of the function to "this" of the component. 24 | //However, when nesting calls, it seems best to use "let that = this;" 25 | // and use "that" in the inner nested calls. 26 | this.getPeers = this.getPeers.bind(this); 27 | this.getVolumes = this.getVolumes.bind(this); 28 | this.handleVolumeRowClick= this.handleVolumeRowClick.bind(this); 29 | } 30 | 31 | componentDidMount(){ 32 | this.getPeers(); 33 | this.getVolumes(); 34 | } 35 | refreshAll = () => { 36 | this.getPeers(); 37 | this.getVolumes(); 38 | this.setState({ selectedVolumes: {} }); 39 | 40 | } 41 | onCancelWizard = (type) => { 42 | if(type == "expandCluster"){ 43 | this.setState({expandClusterStarted: false}); 44 | } 45 | if(type == "createVolume"){ 46 | this.setState({createVolumeStarted: false}); 47 | } 48 | this.refreshAll(); 49 | } 50 | onCancelExpandClusterWizard = (event) =>{ 51 | this.setState({expandClusterStarted: false}); 52 | } 53 | onCancelCreateVolumeWizard = (event) =>{ 54 | this.setState({createVolumeStarted: false}); 55 | } 56 | 57 | generateAuthHeader(){ 58 | //TODO: get secret from fs 59 | let secret = "fake_secret"; 60 | let algorithm = "HS256"; 61 | let app_id = "cockpit-gluster"; 62 | let time = Math.floor(new Date().getTime/1000); 63 | let claims = { 64 | "iss" : app_id, 65 | "iat" : time, 66 | "exp" : time + 120 67 | } 68 | return "bearer " + jwt.sign(claims, secret, {algorithm: algorithm}) 69 | } 70 | 71 | getPeers(){ 72 | let that = this; 73 | let headers = { "Authorization" : this.generateAuthHeader() }; 74 | let promise = this.gluster_api.get("/v1/peers") 75 | promise 76 | .then(function(result){ 77 | let peers = JSON.parse(result); 78 | that.setState({"peers":peers}); 79 | }) 80 | .catch(function(reason){ 81 | console.warn("Failed for reason: ", reason); 82 | }) 83 | return promise 84 | } 85 | 86 | getVolumes(){ 87 | let that = this; 88 | let headers = { "Authorization" : this.generateAuthHeader() }; 89 | // console.log("headers", headers); 90 | let promise = this.gluster_api.get("/v1/volumes") 91 | promise 92 | .then(function(result){ 93 | let volumes = JSON.parse(result); 94 | that.setState({"volumes":volumes}); 95 | }) 96 | .catch(function(reason){ 97 | console.warn("Failed for reason: ", reason); 98 | }) 99 | return promise 100 | } 101 | 102 | getVolumeBricks(volumeName){ 103 | let that = this; 104 | let headers = { "Authorization" : this.generateAuthHeader() }; 105 | let promise = this.gluster_api.get("/v1/volumes/"+volumeName+"/bricks") 106 | promise 107 | .then(function(volumeBricksJson){ 108 | let volumeBrickList = JSON.parse(volumeBricksJson); 109 | that.setState(function(prevState, props){ 110 | if(prevState.volumeBricks == null){ 111 | prevState.volumeBricks = {}; 112 | } 113 | prevState.volumeBricks[volumeName] = volumeBrickList; 114 | return {"volumeBricks": prevState.volumeBricks} 115 | }); 116 | }) 117 | .catch(function(reason){ 118 | console.warn("Failed for reason: ", reason); 119 | }) 120 | return promise 121 | } 122 | 123 | 124 | handleVolumeRowClick(volumeName){ 125 | let that = this; 126 | this.setState(function(prevState, props){ 127 | if(prevState.selectedVolumes.hasOwnProperty(volumeName)){ 128 | delete prevState.selectedVolumes[volumeName]; 129 | return {selectedVolumes:prevState.selectedVolumes} 130 | } 131 | else{ 132 | prevState.selectedVolumes[volumeName]="fetching"; 133 | that.getVolumeBricks(volumeName).then(function(volumeBricks){ 134 | that.setState(function(prevState,props){ 135 | prevState.selectedVolumes[volumeName] = ""; 136 | return {selectedVolumes:prevState.selectedVolumes}; 137 | }); 138 | }); 139 | 140 | return {selectedVolumes:prevState.selectedVolumes} 141 | } 142 | }); 143 | } 144 | 145 | handleExpandCluster = (event) => { 146 | this.setState((prevState)=>{ 147 | if (prevState.expandClusterStarted){ 148 | if(this.expandClusterWizard.current){ 149 | this.expandClusterWizard.current.show(); 150 | } 151 | return { expandClusterStarted: false} 152 | } 153 | return { expandClusterStarted: true} 154 | }) 155 | } 156 | handleCreateVolume = (event) =>{ 157 | this.setState((prevState)=>{ 158 | if (prevState.createVolumeStarted){ 159 | if(this.createVolumeWizard.current){ 160 | this.createVolumeWizard.current.show(); 161 | } 162 | return { createVolumeStarted: false} 163 | } 164 | return { createVolumeStarted: true} 165 | }) 166 | } 167 | 168 | render(){ 169 | return( 170 |
171 |
172 |

Gluster Management

173 |
174 |
175 | { 176 | this.state.peers !== null && 177 | 181 | } 182 | { 183 | this.state.peers == null && 184 | 186 | } 187 |
188 |
189 |
190 |
191 | { 192 | this.state.volumes !== null && 193 | 201 | } 202 | { 203 | this.state.volumes == null && 204 | 206 | } 207 |
208 |
209 | {this.state.expandClusterStarted && {this.onCancelWizard("expandCluster")}} 211 | ref={this.expandClusterWizard} 212 | type="expandCluster" 213 | />} 214 | {this.state.createVolumeStarted && {this.onCancelWizard("createVolume")}} 216 | ref={this.createVolumeWizard} 217 | peers={this.state.peers} 218 | type="createVolume" 219 | />} 220 |
221 |
222 | ) 223 | } 224 | } 225 | 226 | class HostsTable extends Component{ 227 | constructor(props){ 228 | super(props); 229 | } 230 | 231 | generateTable(){ 232 | this.hostTableRows = []; 233 | this.hostTableHeadings =[]; 234 | this.moreInfoModals=[]; 235 | for (let heading of ["Name","Peer status","ID","More Info"]){ 236 | this.hostTableHeadings.push( 237 | {heading} 238 | ) 239 | } 240 | for(let host of this.props.peers){ 241 | this.hostTableRows.push( 242 | 243 | {host.name} 244 | 245 | 246 | {host.online && Online} 247 | {!host.online && Offline} 248 | 249 | {host.id} 250 | 251 | ); 252 | this.moreInfoModals.push( 253 | 254 | ); 255 | } 256 | } 257 | 258 | render(){ 259 | this.generateTable(); 260 | return( 261 |
262 |
263 | Peers 264 | 268 | 269 | 273 | 274 |
275 | 276 | 277 | {this.hostTableHeadings} 278 | 279 | 280 | {this.hostTableRows} 281 | 282 |
283 | {this.moreInfoModals} 284 |
285 | ) 286 | } 287 | } 288 | 289 | class VolumeBricksTable extends Component{ 290 | constructor(props){ 291 | super(props); 292 | } 293 | 294 | generateTable = () => { 295 | this.volumeBricksTableRows = []; 296 | this.brickMoreInfoModals = []; 297 | let modalCounter = 0; 298 | for(let brick of this.props.volumeBrickList){ 299 | let brickInfo = brick.info; 300 | modalCounter++; 301 | this.volumeBricksTableRows.push( 302 | 303 | {brickInfo.path} 304 | {brickInfo.id} 305 | 306 | {brick.online && Online} 307 | {!brick.online && Offline} 308 | 309 | 310 | ); 311 | this.brickMoreInfoModals.push( 312 | 313 | ); 314 | } 315 | } 316 | 317 | render(){ 318 | this.generateTable() 319 | return( 320 |
321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | {this.volumeBricksTableRows} 332 | 333 |
BrickUUIDStatusMore Info
334 | {this.brickMoreInfoModals} 335 |
336 | ) 337 | } 338 | } 339 | 340 | class VolumeTable extends Component{ 341 | constructor(props){ 342 | super(props); 343 | } 344 | 345 | generateTable(){ 346 | this.volumeTableRows = []; 347 | this.moreInfoModals=[]; 348 | for(let volume of this.props.volumes){ 349 | let expanded = this.props.selectedVolumes.hasOwnProperty(volume.name) && this.props.volumeBricks !== null && this.props.volumeBricks[volume.name] !== null && this.props.volumeBricks[volume.name] !== undefined; 350 | this.volumeTableRows.push( 351 | {this.props.handleVolumeRowClick(volume.name)}}> 352 | {expanded && }{!expanded && } 353 | {volume.name} 354 | {volume.type}{volume["arbiter-count"] > 0 && " (with Arbiter)"} 355 | 356 | {/* {volume.volumeBricks == 'ONLINE' ? : } */} 357 | {volume.state} 358 | 359 | 360 | 361 | 362 | 363 | 364 | ); 365 | this.moreInfoModals.push( 366 | 367 | ); 368 | if(expanded){ 369 | //TODO: pass the volume.bricksInfo brickinfo so it can be displayed in the modal 370 | this.volumeTableRows.push( 371 | 372 | 373 | 374 | 375 | 376 | ); 377 | } 378 | } 379 | } 380 | 381 | render(){ 382 | 383 | this.generateTable(); 384 | return( 385 |
386 |
387 | Volumes 388 | 393 | 394 | 399 | 400 |
401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | {this.volumeTableRows} 413 | 414 |
NameVolume TypeStatusActions
415 | {this.moreInfoModals} 416 |
417 | ) 418 | } 419 | } 420 | export default GlusterManagement 421 | -------------------------------------------------------------------------------- /src/components/WizardSteps/BrickStep.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {Grid, Form, FormGroup, FormControl, ControlLabel, HelpBlock, Checkbox, Row, Col} from 'patternfly-react' 3 | import { notEmpty } from '../../lib/validators' 4 | import Dropdown from '../common/Dropdown' 5 | 6 | class BrickStep extends Component{ 7 | constructor(props){ 8 | super(props) 9 | this.state = { 10 | //props: glusterModel 11 | hostIndex:0, 12 | 13 | raidValidation: { 14 | raid_type:{validation:false,validationState:null}, 15 | stripe_size:{validation:false,validationState:null}, 16 | disk_count:{validation:false,validationState:null} 17 | }, 18 | cacheValidation: [] 19 | } 20 | // this.defaultCacheMode = { 21 | // cache: false, 22 | // ssd: "/dev/sdc", 23 | // size: 20, 24 | // mode: "writethrough" 25 | // } 26 | 27 | this.validators = { 28 | stripe_size:(value) => {return notEmpty(value)}, 29 | disk_count:(value) => {return notEmpty(value)} 30 | } 31 | 32 | while(this.state.cacheValidation.length < this.props.glusterModel.cacheConfig.length){ 33 | let hostCacheValidation = {}; 34 | for (let key in this.props.glusterModel.cacheConfig[this.state.hostIndex]){ 35 | hostCacheValidation[key] = {validation:false,validationState:null} 36 | } 37 | this.state.cacheValidation.push(hostCacheValidation); 38 | } 39 | 40 | //for generating bricks from volumes 41 | // this.getDefaultValue = { 42 | // volName: (volume) => {return volume.name}, 43 | // device: (volume) => "/dev/sdb", 44 | // size: (volume) => 100, 45 | // thinPool: (volume) => true, 46 | // mountPoint: (volume) => { 47 | // let mountPointSplit = volume.brickDir.split('/'); 48 | // mountPointSplit.pop(); 49 | // let mountPoint = mountPointSplit.join('/') 50 | // return mountPoint 51 | // }, 52 | // vdo: (volume) => false, 53 | // vdoSize: (volume) => 200 54 | // } 55 | 56 | 57 | this.raidOptions = [ 58 | {name: "JBOD", value:"JBOD"}, 59 | {name: "RAID 5", value:"RAID5"}, 60 | {name: "RAID 6", value:"RAID6"}, 61 | {name: "RAID 10", value:"RAID10"} 62 | ] 63 | this.cacheModeOptions =[ 64 | {name:"writethrough",value:"writethrough"}, 65 | {name:"writeback",value:"writeback"} 66 | ] 67 | // this.props.callback({isValid: this.state.brickValidation.every((isValid)=> isValid)}); 68 | //console.debug("BS.Constructor: bricks", this.state.bricks); 69 | } 70 | 71 | 72 | componentWillMount(){ 73 | //initialize validation 74 | for(let key in this.props.glusterModel.raidConfig){ 75 | if (typeof(this.validators[key]) == 'function'){ 76 | let validation = false; 77 | let value = this.props.glusterModel.raidConfig[key]; 78 | let validator = this.validators[key]; 79 | if (value !== undefined){ 80 | validation = validator(value); 81 | } 82 | this.state.raidValidation[key].validation = validation; 83 | } 84 | } 85 | } 86 | 87 | handleHostSelect = (value, hostIndex) => { 88 | this.setState({hostIndex:hostIndex}); 89 | } 90 | 91 | handleBrickChange = (hostIndex, index, bricks,{brickKey, brickValue, isValid}) => { 92 | let oldBricks = bricks; 93 | 94 | let newBricks = []; 95 | if (oldBricks !== undefined){ 96 | newBricks = oldBricks.slice(); 97 | } 98 | let lastHostIndex = hostIndex; 99 | if (hostIndex == 0){ 100 | lastHostIndex = newBricks.length - 1; 101 | } 102 | 103 | 104 | for (let otherHostIndex = hostIndex; otherHostIndex <= lastHostIndex;otherHostIndex++){ 105 | if (brickKey == "vdo" || brickKey == "vdoSize"){ 106 | let device = newBricks[otherHostIndex][index].device; 107 | for(let brickIndex = 0; brickIndex < newBricks[otherHostIndex].length; brickIndex++){ 108 | if(newBricks[otherHostIndex][brickIndex].device == device){ 109 | newBricks[otherHostIndex][brickIndex][brickKey] = brickValue 110 | } 111 | } 112 | } 113 | newBricks[otherHostIndex][index][brickKey] = brickValue; 114 | } 115 | // console.debug("BS.handleBrickChange",hostIndex,index,brickKey, brickValue, JSON.stringify(newBricks)) 116 | this.props.callback({bricks: newBricks}); 117 | } 118 | 119 | onChangeRaidConfig = (key, value) => { 120 | this.setState((prevState)=>{ 121 | let newState = {}; 122 | let newRaidConfig = Object.assign({},this.props.glusterModel.raidConfig); 123 | let validation = true; 124 | if (typeof(this.validators[key]) == 'function'){ 125 | validation = this.validators[key](value); 126 | } 127 | newRaidConfig[key] = value; 128 | newState.raidValidation = prevState.raidValidation; 129 | newState.raidValidation[key].validation = validation; 130 | console.debug("BS.onChangeRaidConfig: key, value", key, value) 131 | this.props.callback({raidConfig: newRaidConfig}); 132 | return newState 133 | }); 134 | } 135 | onBlurRaidConfig = (key,value) => { 136 | this.setState((prevState)=>{ 137 | let newState = {}; 138 | let validation = true; 139 | if (typeof(this.validators[key]) == 'function'){ 140 | validation = this.validators[key](value); 141 | } 142 | newState.raidValidation = prevState.raidValidation; 143 | newState.raidValidation[key].validation = validation; 144 | newState.raidValidation[key].validationState = validation ? null : 'error'; 145 | //console.debug("BS.newRaidValidationState",newState.raidValidation[key].validationState) 146 | return newState 147 | }); 148 | } 149 | 150 | onChangeCacheConfig = (key, value) => { 151 | this.setState((prevState)=>{ 152 | let newState = {}; 153 | let newCacheConfig = this.props.glusterModel.cacheConfig.slice(); 154 | let validation = true; 155 | if (typeof(this.validators[key]) == 'function'){ 156 | validation = this.validators[key](value); 157 | } 158 | newCacheConfig[prevState.hostIndex][key] = value; 159 | if (prevState.hostIndex == 0){ 160 | for (let hostIndex = 1; hostIndex < newCacheConfig.length; hostIndex++){ 161 | newCacheConfig[hostIndex][key] = value; 162 | } 163 | } 164 | newState.cacheValidation = prevState.cacheValidation; 165 | newState.cacheValidation[prevState.hostIndex][key].validation = validation; 166 | this.props.callback({cacheConfig: newCacheConfig}); 167 | return newState 168 | }); 169 | } 170 | onBlurCacheConfig = (key, value) => { 171 | this.setState((prevState)=>{ 172 | let newState = {}; 173 | 174 | let validation = true; 175 | if (typeof(this.validators[key]) == 'function'){ 176 | validation = this.validators[key](value); 177 | } 178 | newState.cacheValidation = prevState.cacheValidation; 179 | newState.cacheValidation[prevState.hostIndex][key].validation = validation; 180 | return newState 181 | }); 182 | } 183 | 184 | onChangeNormal = (key, event) => { 185 | let value = event.target.value; 186 | this.onChangeRaidConfig(key, value); 187 | } 188 | onBlurNormal = (key, event) => { 189 | let value = event.target.value; 190 | this.onBlurRaidConfig(key, value); 191 | } 192 | 193 | 194 | 195 | render(){ 196 | //console.debug("BS.props.volumes:",this.props.glusterModel.volumes); 197 | console.debug("BS.props.bricks:",this.props.glusterModel.bricks); 198 | 199 | let { hosts, volumes, bricks, cacheConfig } = this.props.glusterModel; 200 | let cacheClassNames = cacheConfig[this.state.hostIndex].cache ? "" : "hidden"; 201 | 202 | let hostOptions = hosts.map((host)=>{ 203 | return {name: host, value:host} 204 | }); 205 | 206 | let brickRows = []; 207 | let volumeCount = volumes.length; 208 | for (let index = 0; index < volumeCount; index++){ 209 | //console.debug("BS.BR.vol."+index, volumes[index]) 210 | //console.debug("BS.BR.brick."+index, bricks[this.state.hostIndex][index]) 211 | brickRows.push( 212 | {this.handleBrickChange(this.state.hostIndex,index,bricks,brickInfo)}} 218 | /> 219 | ); 220 | } 221 | 222 | return ( 223 | 224 | 225 | 226 | 227 | 228 | Raid Info: 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | Raid Type 238 | 239 | 240 | 241 |
242 | 243 | {this.onChangeRaidConfig("raid_type", value)}} 247 | /> 248 | 249 |
250 | 251 |
252 | 253 | 254 | 255 | Stripe Size(KB) 256 | 257 | 258 | 259 |
260 | 261 | {this.onChangeNormal("stripe_size",event)}} 264 | onBlur={(event)=>{this.onBlurNormal("stripe_size",event)}} 265 | /> 266 | 267 |
268 | 269 |
270 | 271 | 272 | 273 | Data Disk Count 274 | 275 | 276 | 277 |
278 | 279 | {this.onChangeNormal("disk_count",event)}} 282 | onBlur={(event)=>{this.onBlurNormal("disk_count",event)}} 283 | /> 284 | 285 |
286 | 287 |
288 | 289 | 290 | 291 | 292 | 293 | Brick Configuration: 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | Select Hosts: 302 | 303 | 304 | 305 |
306 | 307 | 311 | 312 |
313 | 314 |
315 | 316 | 317 | 318 |
319 |
320 | 321 |
322 | 323 | 324 | 325 | {brickRows} 326 | 327 | 328 | 329 | 330 | 331 |
332 | 333 | { 337 | this.onChangeCacheConfig("cache",event.target.checked) 338 | }} 339 | /> 340 | 341 | 342 | Configure LV Cache 343 | 344 |
345 | 346 |
347 | 348 | 349 | 350 | SSD 351 | 352 | 353 | 354 |
355 | 356 | {this.onChangeCacheConfig("ssd",event.target.value)}} 359 | onBlur={(event)=>{this.onBlurCacheConfig("ssd",event.target.value)}} 360 | /> 361 | 362 |
363 | 364 |
365 | 366 | 367 | 368 | LV Size(KB) 369 | 370 | 371 | 372 |
373 | 374 | {this.onChangeCacheConfig("size",event.target.value)}} 377 | onBlur={(event)=>{this.onBlurCacheConfig("size",event.target.value)}} 378 | /> 379 | 380 |
381 | 382 |
383 | 384 | 385 | 386 | Cache Mode 387 | 388 | 389 | 390 |
391 | 392 | {this.onChangeCacheConfig("mode", value)}} 395 | /> 396 | 397 |
398 | 399 |
400 |
401 | ); 402 | } 403 | } 404 | 405 | 406 | class BrickRow extends Component { 407 | constructor(props){ 408 | super(props); 409 | this.state = { 410 | validation: { 411 | volName: {validation:false,validationState:null}, 412 | device:{validation:false,validationState:null}, 413 | size: {validation:false,validationState:null}, 414 | thinPool:{validation:false,validationState:null}, 415 | mountPoint:{validation:false,validationState:null}, 416 | vdo: {validation:false,validationState:null}, 417 | vdoSize: {validation:false,validationState:null} 418 | } 419 | } 420 | 421 | this.validators = { 422 | volName: (value) => notEmpty(value), 423 | device: (value) => notEmpty(value), 424 | size: (value) => notEmpty(value), 425 | mountPoint: (value) => notEmpty(value), 426 | } 427 | 428 | for(let key in this.validators){ 429 | let validation = false; 430 | let value = this.props.brick[key] 431 | let validator = this.validators[key]; 432 | if (typeof(validator) == 'function' && value !== undefined){ 433 | validation = validator(value) 434 | } 435 | this.state.validation[key].validation = validation; 436 | } 437 | 438 | } 439 | 440 | onChange = (brick, key, value) => { 441 | this.setState((prevState)=>{ 442 | let newState = {}; 443 | let validator = this.validators[key]; 444 | newState.validation = prevState.validation; 445 | if (typeof(validator) == 'function'){ 446 | newState.validation[key].validation = validator(value); 447 | if(newState.validation[key].validation){ 448 | newState.validation[key].validationState = null; 449 | } 450 | } 451 | this.props.callback({brickKey: key, brickValue: value}); 452 | return newState 453 | }); 454 | } 455 | 456 | onBlur = (brick, key, value) => { 457 | //console.debug("BR.onBlur",key,value); 458 | this.setState((prevState)=>{ 459 | let newState = {}; 460 | let validator = this.validators[key]; 461 | newState.validation = prevState.validation; 462 | if (typeof(validator) == 'function'){ 463 | newState.validation[key].validation = this.validators[key](value); 464 | newState.validation[key].validationState = newState.validation[key].validation ? null : 'error'; 465 | } 466 | return newState 467 | }); 468 | } 469 | 470 | 471 | 472 | render(){ 473 | let brick = this.props.brick; 474 | //console.debug("BR.render brick",brick) 475 | const gridValues=[2,2,1,1,2,2,1]; 476 | return( 477 | 478 | {this.props.index == 0 && 479 | 480 | Volume Name 481 | 482 | 483 | Device Name 484 | 485 | 486 | Size(GB) 487 | 488 | 489 | Thinp 490 | 491 | 492 | Mount Point 493 | 494 | 495 | Dedupe & Compression 496 | 497 | 498 | Virtual Size 499 | 500 | } 501 | 502 | 503 |
504 | 505 | { 510 | this.onChange(brick, "volName", event.target.value) 511 | }} 512 | onBlur={(event)=>{ 513 | this.onBlur(brick, "volName",event.target.value) 514 | }} 515 | /> 516 | 517 |
518 | 519 | 520 |
521 | 522 | { 525 | this.onChange(brick, "device", event.target.value) 526 | }} 527 | onBlur={(event)=>{ 528 | this.onBlur(brick, "device",event.target.value) 529 | }} 530 | /> 531 | 532 |
533 | 534 | 535 |
536 | 537 | { 540 | this.onChange(brick, "size", event.target.value) 541 | }} 542 | onBlur={(event)=>{ 543 | this.onBlur(brick, "size",event.target.value) 544 | }} 545 | /> 546 | 547 |
548 | 549 | 550 |
551 | 552 | {this.onChange(brick, "thinPool",event.target.checked)}} 556 | /> 557 | 558 |
559 | 560 | 561 |
562 | 563 | { 568 | this.onChange(brick, "mountPoint", event.target.value) 569 | }} 570 | onBlur={(event)=>{ 571 | this.onBlur(brick, "mountPoint",event.target.value) 572 | }} 573 | /> 574 | 575 |
576 | 577 | 578 |
579 | 580 | {this.onChange(brick, "vdo",event.target.checked)}} 584 | /> 585 | 586 |
587 | 588 | {brick["vdo"] && 589 |
590 | 591 | { 595 | this.onChange(brick, "vdoSize", event.target.value) 596 | }} 597 | onBlur={(event)=>{ 598 | this.onBlur(brick, "vdoSize",event.target.value) 599 | }} 600 | /> 601 | 602 |
603 | } 604 |
605 |
606 | ); 607 | } 608 | } 609 | 610 | 611 | export default BrickStep 612 | -------------------------------------------------------------------------------- /src/components/WizardSteps/HostStep.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {Grid, Form, FormGroup, FormControl, ControlLabel, HelpBlock} from 'patternfly-react' 3 | 4 | 5 | class HostStep extends Component{ 6 | constructor(props){ 7 | super(props) 8 | this.state = { 9 | hostValidation: this.props.glusterModel.hosts.map((host)=>this.validateHost(host)), 10 | } 11 | this.props.callback({isValid: this.state.hostValidation.every((isValid)=> isValid)}); 12 | //console.debug("HS.constructor, hosts:", this.props.glusterModel.hosts); 13 | } 14 | 15 | onHostChanged = (hostIndex,event) => { 16 | //console.debug("HS.onHC triggered!") 17 | let hostValue = event.target.value; 18 | this.setState((prevState)=>{ 19 | let newHosts=this.props.glusterModel.hosts.slice() 20 | //console.debug("HS.onHC newHosts", newHosts) 21 | newHosts[hostIndex] = hostValue; 22 | prevState.hostValidation[hostIndex] = this.validateHost(hostValue); 23 | let state = { 24 | hostValidation: prevState.hostValidation, 25 | }; 26 | 27 | const isValid = state.hostValidation.every((isValid)=> isValid); 28 | this.props.callback({hosts: newHosts, isValid: isValid}); 29 | return state; 30 | }) 31 | } 32 | 33 | validateHost = (hostValue) => { 34 | if (hostValue.length > 0){ 35 | return true 36 | } 37 | else{ 38 | return false 39 | } 40 | } 41 | 42 | getHostValidationState = (index) => { 43 | if (this.props.showValidation){ 44 | if(this.state.hostValidation[index]){ 45 | return null 46 | } 47 | return 'error' 48 | } 49 | return null 50 | } 51 | 52 | 53 | render(){ 54 | //console.debug("inSideHS, gm.hosts:",this.props.glusterModel.hosts) 55 | let hostInputs = []; 56 | let hostCount = this.props.glusterModel.hosts.length; 57 | //console.debug(hostCount) 58 | for (let index = 0; index < hostCount; index++){ 59 | let isArbiterHost = index == hostCount - 1; 60 | hostInputs.push( 61 | 62 | Host {index+1} 63 | {this.onHostChanged(index,event)}} 65 | /> 66 | {isArbiterHost && This host will be used as the arbiter if arbiter is configured.} 67 | 68 | ); 69 | } 70 | return ( 71 | 72 | 73 | 74 | {hostInputs} 75 |
76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | HostStep.title = "exported title" 85 | 86 | export default HostStep 87 | -------------------------------------------------------------------------------- /src/components/WizardSteps/ReviewStep.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Grid, 4 | Row, 5 | Col, 6 | Form, 7 | FormGroup, 8 | FormControl, 9 | ControlLabel, 10 | HelpBlock, 11 | Checkbox 12 | } from 'patternfly-react' 13 | import { notEmpty } from '../../lib/validators' 14 | import Dropdown from '../common/Dropdown' 15 | import yaml from 'js-yaml'; 16 | const INVENTORY = `/etc/ansible/hc_wizard_inventory.yml`; 17 | 18 | class ReviewStep extends Component { 19 | constructor(props){ 20 | super(props) 21 | this.state = { 22 | isEditing: false, 23 | inventory: this.generateInventory(this.props.glusterModel), 24 | } 25 | this.writeFile(this.state.inventory, INVENTORY); 26 | this.deploymentStreamTextArea = React.createRef(); 27 | } 28 | 29 | componentDidUpdate = (prevProps, prevState) => { 30 | if(prevProps.deploymentStream !== this.props.deploymentStream){ 31 | if(this.deploymentStreamTextArea.current){ 32 | this.deploymentStreamTextArea.current.scrollTop = this.deploymentStreamTextArea.current.scrollHeight; 33 | } 34 | else{ 35 | console.warn("no Ref for deploymentStream!") 36 | } 37 | } 38 | } 39 | 40 | 41 | 42 | 43 | handleEmpty = (po) => { 44 | return JSON.stringify(po) == '{}' ? "" : po; 45 | } 46 | uniqueStringsArray = (arr) => { 47 | var u = {}, a = []; 48 | for(var i = 0, l = arr.length; i < l; ++i){ 49 | if(!u.hasOwnProperty(arr[i])) { 50 | a.push(arr[i]); 51 | u[arr[i]] = 1; 52 | } 53 | } 54 | return a; 55 | } 56 | generateInventory = (glusterModel) =>{ 57 | let groups = {}; 58 | let { hosts, volumes, bricks, raidConfig, cacheConfig } = glusterModel; 59 | groups.hc_nodes = {}; 60 | groups.hc_nodes.hosts = {}; 61 | groups.local = { hosts: {localhost: null}}; 62 | let groupVars = {} 63 | let localVars = {} 64 | groupVars.gluster_infra_stripe_unit_size = raidConfig.stripe_size; 65 | groupVars.gluster_infra_disktype = raidConfig.raid_type; 66 | groupVars.gluster_infra_diskcount = raidConfig.disk_count; 67 | 68 | 69 | for (let hostIndex = 0; hostIndex < hosts.length;hostIndex++){ 70 | let hostVars = {}; 71 | hostVars.gluster_infra_volume_groups = []; 72 | hostVars.gluster_infra_mount_devices = []; 73 | let processedDevs = {}; // VG and VDO is processed once per device processedVG implies processedVDO 74 | let hostBricks = bricks[hostIndex]; 75 | let hostCacheConfig = cacheConfig[hostIndex]; 76 | 77 | for (let brick of hostBricks){ 78 | let devName = brick.device.split("/").pop(); 79 | let pvName = brick.device; //changes if vdo 80 | let vgName = `vg_${devName}`; 81 | let thinpoolName = `thinpool_${vgName}`; 82 | let lvName = `gluster_lv_${brick.volName}`; 83 | let isDevProcessed = Object.keys(processedDevs).indexOf(devName) > -1; 84 | let isThinpoolCreated = false; 85 | if(isDevProcessed){ 86 | isThinpoolCreated = processedDevs[devName]['thinpool']; 87 | } 88 | let isVDO = brick.vdo == true && brick.vdoSize; 89 | //TODO: cache more than one device. 90 | if(hostCacheConfig.cache && hostVars.gluster_infra_cache_vars == undefined){ 91 | hostVars.gluster_infra_cache_vars = [{ 92 | vgname: vgName, 93 | cachedisk: hostCacheConfig.ssd, 94 | cachelvname: `cachelv_${thinpoolName}`, 95 | cachethinpoolname: thinpoolName, 96 | cachelvsize: `${hostCacheConfig.size - (hostCacheConfig.size/10)}G`, 97 | cachemetalvsize: `${hostCacheConfig.size/10}G`, 98 | cachemetalvname: `cache_${thinpoolName}`, 99 | cachemode: hostCacheConfig.mode 100 | }]; 101 | } 102 | if(isVDO){ 103 | let vdoName = `vdo_${devName}` 104 | pvName = `/dev/mapper/${vdoName}`; 105 | if(hostVars.gluster_infra_vdo == undefined){ 106 | hostVars.gluster_infra_vdo = []; 107 | hostVars.gluster_infra_vdo_blockmapcachesize = "128M"; 108 | hostVars.gluster_infra_vdo_slabsize = (brick.vdoSize <= 1000) ? "2G": "32G"; 109 | hostVars.gluster_infra_vdo_readcachesize = "20M"; 110 | hostVars.gluster_infra_vdo_readcache = "enabled"; 111 | hostVars.gluster_infra_vdo_writepolicy = "auto"; 112 | hostVars.gluster_infra_vdo_emulate512 = "on"; 113 | } 114 | 115 | if(!isDevProcessed){ 116 | hostVars.gluster_infra_vdo.push({ 117 | name: vdoName, 118 | device: brick.device, 119 | logicalsize: `${brick.vdoSize}G` 120 | }); 121 | } 122 | } 123 | 124 | if(!isDevProcessed){ 125 | //create vg 126 | hostVars.gluster_infra_volume_groups.push({ 127 | vgname: vgName, 128 | pvname: pvName 129 | }); 130 | } 131 | 132 | if(brick.thinPool && !isThinpoolCreated){ 133 | if(hostVars.gluster_infra_thinpools == undefined){ 134 | hostVars.gluster_infra_thinpools = []; 135 | } 136 | hostVars.gluster_infra_thinpools.push({ 137 | vgname: vgName, 138 | thinpoolname: thinpoolName, 139 | poolmetadatasize: '16G' 140 | }); 141 | isThinpoolCreated = true; 142 | } 143 | 144 | if(brick.thinPool){ 145 | if(hostVars.gluster_infra_lv_logicalvols == undefined){ 146 | hostVars.gluster_infra_lv_logicalvols = []; 147 | } 148 | hostVars.gluster_infra_lv_logicalvols.push({ 149 | vgname: vgName, 150 | thinpool: thinpoolName, 151 | lvname: lvName, 152 | lvsize: `${brick.size}G` 153 | }); 154 | } 155 | if(!brick.thinPool){ 156 | if(hostVars.gluster_infra_thick_lvs == undefined){ 157 | hostVars.gluster_infra_thick_lvs = []; 158 | } 159 | hostVars.gluster_infra_thick_lvs.push({ 160 | vgname: vgName, 161 | lvname: lvName, 162 | size: `${brick.size}G` 163 | }); 164 | } 165 | hostVars.gluster_infra_mount_devices.push({ 166 | path: brick.mountPoint, 167 | lvname: lvName, 168 | vgname: vgName 169 | }); 170 | processedDevs[devName] = {thinpool: isThinpoolCreated} 171 | } 172 | groups.hc_nodes.hosts[hosts[hostIndex]] = hostVars; 173 | } 174 | 175 | localVars.gluster_features_hci_volumes = []; 176 | for(let volumeIndex = 0; volumeIndex < volumes.length;volumeIndex++){ 177 | let volume = volumes[volumeIndex]; 178 | localVars.gluster_features_hci_volumes.push({ 179 | volname: volume.name, 180 | brick: volume.brickDir, 181 | arbiter: volume.isArbiter, 182 | master: "localhost" 183 | }); 184 | } 185 | const SSH_OPTS = '-o StrictHostKeyChecking=no' 186 | 187 | localVars.ansible_ssh_common_args = SSH_OPTS; 188 | groupVars.ansible_ssh_common_args = SSH_OPTS; 189 | 190 | groups.hc_nodes.vars = groupVars; 191 | groups.local.vars = localVars; 192 | 193 | return yaml.safeDump(groups) 194 | 195 | } 196 | handleEdit = (event) => { 197 | this.setState({isEditing:true}); 198 | } 199 | handleReload = () => { 200 | this.readInventory(INVENTORY).then((content,tag)=>{ 201 | this.setState({inventory:content}) 202 | }) 203 | } 204 | 205 | readInventory = (path) => { 206 | let file = cockpit.file(path) 207 | return file.read().done((content,tag)=>{ 208 | if(content== null){ 209 | console.warn(`File: ${path} does not exist`) 210 | } 211 | }).fail((error)=>{ 212 | 213 | console.warn(`File: ${path} not written`,error) 214 | }).always((tag) => { 215 | file.close() 216 | }); 217 | } 218 | 219 | 220 | 221 | writeFile = (inventory, path) =>{ 222 | let file = cockpit.file(path) 223 | return file.replace(inventory).done((content,tag)=>{ 224 | if(content== null){ 225 | console.warn(`File: ${path} does not exist`) 226 | } 227 | }).fail((error)=>{ 228 | 229 | console.warn(`File: ${path} not written`,error) 230 | }).always((tag) => { 231 | file.close() 232 | }); 233 | } 234 | handleSave = () => { 235 | this.setState((prevState)=>{ 236 | this.writeFile(prevState.inventory, INVENTORY); 237 | return {isEditing:false} 238 | }); 239 | } 240 | 241 | handleTextChange = (event) => { 242 | this.setState({inventory: event.target.value}); 243 | } 244 | render(){ 245 | let deploymentDone = this.props.deploymentState == "failed" || this.props.deploymentState == "done"; 246 | let showOutput = this.props.isDeploymentStarted || (deploymentDone && !this.props.isRetry); 247 | 248 | return ( 249 | 250 | 251 | 252 |
253 |
254 | 255 | 256 | Generated ansible inventory : {this.props.configFilePath} 257 | 258 | {this.props.isDeploymentStarted && [Running Play]} 259 |
260 | {this.state.isEditing && !this.props.isDeploymentStarted && 261 | 266 | } 267 | {!showOutput && !this.state.isEditing && !this.props.isDeploymentStarted && 268 | 273 | } 274 | {!showOutput && !this.props.isDeploymentStarted && } 279 |
280 |
281 | { !showOutput && 282 |