diff --git a/docs/imgs/vsphere-active.png b/docs/imgs/vsphere-active.png new file mode 100644 index 000000000..650907d1f Binary files /dev/null and b/docs/imgs/vsphere-active.png differ diff --git a/docs/imgs/vsphere-basicSetting.png b/docs/imgs/vsphere-basicSetting.png new file mode 100644 index 000000000..f356193e2 Binary files /dev/null and b/docs/imgs/vsphere-basicSetting.png differ diff --git a/docs/imgs/vsphere-pending.png b/docs/imgs/vsphere-pending.png new file mode 100644 index 000000000..b91c42717 Binary files /dev/null and b/docs/imgs/vsphere-pending.png differ diff --git a/docs/imgs/vsphere-select.png b/docs/imgs/vsphere-select.png new file mode 100644 index 000000000..cd6e16cb1 Binary files /dev/null and b/docs/imgs/vsphere-select.png differ diff --git a/docs/imgs/vsphere-vcSetting.png b/docs/imgs/vsphere-vcSetting.png new file mode 100644 index 000000000..dc39975e4 Binary files /dev/null and b/docs/imgs/vsphere-vcSetting.png differ diff --git a/docs/imgs/vsphere-vmSetting.png b/docs/imgs/vsphere-vmSetting.png new file mode 100644 index 000000000..9b4f14074 Binary files /dev/null and b/docs/imgs/vsphere-vmSetting.png differ diff --git a/docs/vsphere.md b/docs/vsphere.md new file mode 100644 index 000000000..7a93f47eb --- /dev/null +++ b/docs/vsphere.md @@ -0,0 +1,108 @@ +# vSphere type host creation guide + +## Prerequisites + +- Make sure time is in sync on all esxi hosts in the cluster will be used by Cello otherwise deployment may fail due to certificate expiry check. +- Cello vSphere agent is tested on vSphere deployments with vCenter, single node vSphere without vCenter is not supported. +- Deployment requires DHCP server in the VM network.(If you create VM that use VM network and it can get IP automatically which mean it has a DHCP server) +- vCenter user with following minimal set of privileges is required. + - Datastore > Allocate space + - Datastore > Low level file Operations + - Folder > Create Folder + - Folder > Delete Folder + - Network > Assign network + - Resource > Assign virtual machine to resource pool + - Virtual machine > Configuration > Add new disk + - Virtual Machine > Configuration > Add existing disk + - Virtual Machine > Configuration > Add or remove device + - Virtual Machine > Configuration > Change CPU count + - Virtual Machine > Configuration > Change resource + - Virtual Machine > Configuration > Memory + - Virtual Machine > Configuration > Modify device settings + - Virtual Machine > Configuration > Remove disk + - Virtual Machine > Configuration > Rename + - Virtual Machine > Configuration > Settings + - Virtual machine > Configuration > Advanced + - Virtual Machine > Interaction > Power off + - Virtual Machine > Interaction > Power on + - Virtual Machine > Inventory > Create from existing + - Virtual Machine > Inventory > Create new + - Virtual Machine > Inventory > Remove + - Virtual Machine > Provisioning > Clone virtual machine + - Virtual Machine > Provisioning > Customize + - Virtual Machine > Provisioning > Read customization specifications + - vApp > ImportProfile-driven + - storage -> Profile-driven storage view + +## Deployment + +**Note** : + +The vSphere agent is tested on vCenter 6.0 and vCenter 6.5 + +### Upload VM image to be used to vSphere: + +Upload the template OS OVA to vCenter before create vSphere type host in Cello. The work nodes that run the fabric workloads will be cloned from this VM template. + +#### Upload using vSphere Client. + +1. Login to vSphere Client. +2. Right-Click on ESX host on which you want to deploy template. +3. Select Deploy OVF template. +4. Copy and paste URL for [OVA for Cello](https://drive.google.com/file/d/0B4Ioua6jjCH9b0ROOE14SUlqUk0/view?usp=sharing) +5. Please deploy the ova on the same cluster which will be planned to be used by cello later +6. **Check the name of the VM created** , this will be used to create vSphere type host in Cello later. (Should default to PhotonOSTemplate.ova) + +This OVA is based on Photon OS(v2.0) + +**NOTE: DO NOT POWER ON THE IMPORTED VM.** + +If you do power it on, future clones of this VM will end up [getting the same IP as the imported VM](https://github.com/vmware/photon/wiki/Frequently-Asked-Questions#q-why-do-all-of-my-cloned-photon-os-instances-have-the-same-ip-address-when-using-dhcp). To work around this run the following command before powering the VM off. + +echo -n > /etc/machine-id + +## Add vSphere type host + +Login to Cello and navigate to the Hosts->Add Host + +In the "Add a host" page select "Host Type" as VSPHERE + + ![select vsphere](imgs/vsphere-select.png) + +Give a name of the vSphere host like "cello-vsphere", you can specify a capacity number, this number can be configured later. Click "Next" + + ![basic setting](imgs/vsphere-basicSetting.png) + +In the VC address field input your VC IP address, if your VC use some port other than 443, you should add the port eg. 10.112.125.53:8443 + +In the VC user field input a username with the privileges as described in Prerequisites. + +In the VC network field enter the network name. + +In the Datacenter field enter the datacenter name. + +In the Cluster field enter the cluster name that will be used for Cello, this cluster need to belong to the datacenter entered in the Datacenter field. + +In the datastore field, enter a datastore that all the esxi hosts in the cluster can be reachable. + + ![vc setting](imgs/vsphere-vcSetting.png) + +In the VM IP field enter a static IP address that belong to the VM network. + +In the VM Gateway filed enter the VM network gateway. + +In the VM Netmask field enter the network mask for the VM network. + +In the VM DNS filed enter the DNS for the VM network. + +In the VM template field enter the template name (If you don't change the name it will be PhotonOSTemplate.ova by default) + + ![vm setting](imgs/vsphere-vmSetting.png) + +Click Create. You will see the follow page. + +. ![vc pending](imgs/vsphere-pending.png) + +This means that the vSphere agent is trying to create a new work node. This will take several minutes which is depend on your esxi host's resource and network speed. After the node created it will automatically download the farbic 1.0 images to the work node. When everything is ready the new host will be in active state. + + ![vc active](imgs/vsphere-active.png) diff --git a/src/resources/host_api.py b/src/resources/host_api.py index b148b97cd..eb51ee78a 100644 --- a/src/resources/host_api.py +++ b/src/resources/host_api.py @@ -6,6 +6,7 @@ import logging import os import sys +import uuid from flask import jsonify, Blueprint, render_template from flask import request as r @@ -70,16 +71,61 @@ def host_create(): else: schedulable = "false" - logger.debug("name={}, worker_api={}, capacity={}" - "fillup={}, schedulable={}, log={}/{}". - format(name, worker_api, capacity, autofill, schedulable, - log_type, log_server)) - if not name or not worker_api or not capacity or not log_type: - error_msg = "host POST without enough data" - logger.warning(error_msg) - return make_fail_resp(error=error_msg, data=r.form) - else: - host_type = host_type if host_type else detect_daemon_type(worker_api) + if host_type == "vsphere": + vcaddress = r.form['vc_address'] + if vcaddress.find(":") == -1: + address = vcaddress + port = "443" + else: + address = vcaddress.split(':')[0] + port = vcaddress.split(':')[1] + logger.debug("address={}, port={}".format(address, port)) + + vmname = "cello-vsphere-" + str(uuid.uuid1()) + vsphere_param = { + 'vc': { + 'address': address, + 'port': port, + 'username': r.form['vc_user'], + 'password': r.form['vc_password'], + 'network': r.form['vc_network'], + 'vc_datastore': r.form['datastore'], + 'vc_datacenter': r.form['datacenter'], + 'vc_cluster': r.form['cluster'], + 'template': r.form['vm_template']}, + 'vm': { + 'vmname': vmname, + 'ip': r.form['vm_ip'], + 'gateway': r.form['vm_gateway'], + 'netmask': r.form['vm_netmask'], + 'dns': r.form['vm_dns'], + 'vcpus': int(r.form['vm_cpus']), + 'memory': int(r.form['vm_memory'])}} + + logger.debug("name={}, capacity={}" + "fillup={}, schedulable={}, log={}/{}, vsphere_param={}". + format(name, capacity, autofill, schedulable, + log_type, log_server, vsphere_param)) + + vsphere_must_have_params = { + 'Name': name, + 'Capacity': capacity, + 'LoggingType': log_type, + 'VCAddress': address, + 'VCUser': r.form['vc_user'], + 'VCPassword': r.form['vc_password'], + 'VCNetwork': r.form['vc_network'], + 'Datastore': r.form['datastore'], + 'Datacenter': r.form['datacenter'], + 'Cluster': r.form['cluster'], + 'VMIp': r.form['vm_ip'], + 'VMGateway': r.form['vm_gateway'], + 'VMNetmask': r.form['vm_netmask']} + for key in vsphere_must_have_params: + if vsphere_must_have_params[key] == '': + error_msg = "host POST without {} data".format(key) + logger.warning(error_msg) + return make_fail_resp(error=error_msg, data=r.form) result = host_handler.create(name=name, worker_api=worker_api, capacity=int(capacity), autofill=autofill, @@ -87,14 +133,41 @@ def host_create(): log_level=log_level, log_type=log_type, log_server=log_server, - host_type=host_type) - if result: - logger.debug("host creation successfully") - return make_ok_resp(code=CODE_CREATED) - else: - error_msg = "Failed to create host {}".format(r.form["name"]) + host_type=host_type, + params=vsphere_param) + else: + logger.debug("name={}, worker_api={}, capacity={}" + "fillup={}, schedulable={}, log={}/{}". + format(name, worker_api, capacity, autofill, schedulable, + log_type, log_server)) + if not name or not worker_api or not capacity or not log_type: + error_msg = "host POST without enough data" logger.warning(error_msg) - return make_fail_resp(error=error_msg) + return make_fail_resp(error=error_msg, data=r.form) + else: + host_type = host_type if host_type \ + else detect_daemon_type(worker_api) + result = host_handler.create(name=name, worker_api=worker_api, + capacity=int(capacity), + autofill=autofill, + schedulable=schedulable, + log_level=log_level, + log_type=log_type, + log_server=log_server, + host_type=host_type) + logger.debug("result.msg={}".format(result.get('msg'))) + if (host_type == "vsphere") and ('msg' in result): + vsphere_errmsg = result.get('msg') + error_msg = "Failed to create vsphere host {}".format(vsphere_errmsg) + logger.warning(error_msg) + return make_fail_resp(error=error_msg) + elif result: + logger.debug("host creation successfully") + return make_ok_resp(code=CODE_CREATED) + else: + error_msg = "Failed to create host {}".format(r.form["name"]) + logger.warning(error_msg) + return make_fail_resp(error=error_msg) @bp_host_api.route('/host', methods=['PUT']) diff --git a/src/themes/basic/static/css/vsphere.step.css b/src/themes/basic/static/css/vsphere.step.css new file mode 100644 index 000000000..5482a8053 --- /dev/null +++ b/src/themes/basic/static/css/vsphere.step.css @@ -0,0 +1,59 @@ +.steps { + list-style: none; + display: table; + width: 100%; + padding: 0; + margin: 0; + position: relative; +} +.steps li { + display: table-cell; + text-align: center; + width: 1%; +} +.steps li .step { + border: 2px solid #ced1d6; + font-size: 15px; + border-radius: 100%; + background-color: #FFF; + position: relative; + z-index: 2; + display: inline-block; + width: 30px; + height: 30px; + line-height: 25px; +} +.steps li:before { + display: block; + content: ""; + width: 100%; + height: 7px; + font-size: 0; + overflow: hidden; + border-top: 2px solid #CED1D6; + position: relative; + top: 21px; + z-index: 1; +} +.steps li.last-child:before { + max-width: 50%; + width: 50%; +} +.steps li:last-child:before { + max-width: 50%; + width: 50%; +} +.steps li:first-child:before { + max-width: 51%; + left: 50%; +} +.steps li.active:before, +.steps li.active .step{ + border-color: #428bca; +} +.steps li .title { + display: block; + margin-top: 2px; + max-width: 100%; + font-size: 12px; +} diff --git a/src/themes/basic/static/js/script.js b/src/themes/basic/static/js/script.js index d57e9c27b..18bfcafe7 100644 --- a/src/themes/basic/static/js/script.js +++ b/src/themes/basic/static/js/script.js @@ -60,6 +60,7 @@ $(document).ready(function () { //var name = $(this).parents('form:first').find('[name="name"]').val(); //var worker_api = $(this).parents('form:first').find('[name="worker_api"]').val(); var form_data = $('#add_new_host_form').serialize(); + $("#host_create_button").attr('disabled',true); $.ajax({ url: "/api/host", @@ -70,12 +71,14 @@ $(document).ready(function () { console.log(response); $('#newHostModal').hide(); alertMsg('Success!', 'New host is created.', 'success'); + $("#host_create_button").attr('disabled',false); setTimeout("location.reload(true);", 2000); }, error: function (error) { console.log(error); $('#newHostModal').hide(); alertMsg('Failed!', error.responseJSON.error, 'danger'); + $("#host_create_button").attr('disabled',false); setTimeout("location.reload(true);", 2000); } }); diff --git a/src/themes/basic/static/js/vsphere.step.js b/src/themes/basic/static/js/vsphere.step.js new file mode 100644 index 000000000..38fb7848f --- /dev/null +++ b/src/themes/basic/static/js/vsphere.step.js @@ -0,0 +1,133 @@ +function show_vsphere_setting(value){ + if(value=="vsphere"){ + document.getElementById("vsphere_step_div").style.display="block"; + document.getElementById("host_worker_api_div").style.display="none"; + document.getElementById("host_create_button").style.display="none"; + document.getElementById("host_cancel_button").style.display="none"; + document.getElementById("vsphere_step_prev").style.display="inline-block"; + document.getElementById("vsphere_step_next").style.display="inline-block"; + document.getElementById("vc_address").style.display="inline-block"; + document.getElementById("vc_user").style.display="inline-block"; + document.getElementById("vc_password").style.display="inline-block"; + document.getElementById("vc_network").style.display="inline-block"; + document.getElementById("datastore").style.display="inline-block"; + document.getElementById("datacenter").style.display="inline-block"; + document.getElementById("cluster").style.display="inline-block"; + document.getElementById("vm_ip").style.display="inline-block"; + document.getElementById("vm_gateway").style.display="inline-block"; + document.getElementById("vm_netmask").style.display="inline-block"; + document.getElementById("vm_dns").style.display="inline-block"; + document.getElementById("vm_cpus").style.display="inline-block"; + document.getElementById("vm_memory").style.display="inline-block"; + document.getElementById("vm_template").style.display="inline-block"; + $("#vc_address").attr('disabled',false); + $("#vc_user").attr('disabled',false); + $("#vc_password").attr('disabled',false); + $("#vc_network").attr('disabled',false); + $("#datastore").attr('disabled',false); + $("#datacenter").attr('disabled',false); + $("#cluster").attr('disabled',false); + $("#vm_ip").attr('disabled',false); + $("#vm_gateway").attr('disabled',false); + $("#vm_netmask").attr('disabled',false); + $("#vm_dns").attr('disabled',false); + $("#vm_cpus").attr('disabled',false); + $("#vm_memory").attr('disabled',false); + $("#vm_template").attr('disabled',false); + } + else{ + document.getElementById("vsphere_step_div").style.display="none"; + document.getElementById("host_worker_api_div").style.display="block"; + document.getElementById("host_create_button").style.display="inline-block"; + document.getElementById("host_cancel_button").style.display="inline-block"; + document.getElementById("vsphere_step_prev").style.display="none"; + document.getElementById("vsphere_step_next").style.display="none"; + document.getElementById("vc_address").style.display="none"; + document.getElementById("vc_user").style.display="none"; + document.getElementById("vc_password").style.display="none"; + document.getElementById("vc_network").style.display="none"; + document.getElementById("datastore").style.display="none"; + document.getElementById("datacenter").style.display="none"; + document.getElementById("cluster").style.display="none"; + document.getElementById("vm_ip").style.display="none"; + document.getElementById("vm_gateway").style.display="none"; + document.getElementById("vm_netmask").style.display="none"; + document.getElementById("vm_dns").style.display="none"; + document.getElementById("vm_cpus").style.display="none"; + document.getElementById("vm_memory").style.display="none"; + document.getElementById("vm_template").style.display="none"; + $("#vc_address").attr('disabled',true); + $("#vc_user").attr('disabled',true); + $("#vc_password").attr('disabled',true); + $("#vc_network").attr('disabled',true); + $("#datastore").attr('disabled',true); + $("#datacenter").attr('disabled',true); + $("#cluster").attr('disabled',true); + $("#vm_ip").attr('disabled',true); + $("#vm_gateway").attr('disabled',true); + $("#vm_netmask").attr('disabled',true); + $("#vm_dns").attr('disabled',true); + $("#vm_cpus").attr('disabled',true); + $("#vm_memory").attr('disabled',true); + $("#vm_template").attr('disabled',true); + } +} + +var CurStep = 1; +function PrevClick(){ + if(CurStep == 2){ + CurStep = 1; + document.getElementById("host_name_div").style.display="block"; + document.getElementById("other_setting_div").style.display="block"; + document.getElementById("vc_setting_div").style.display="none"; + document.getElementById("vm_setting_div").style.display="none"; + $("#vsphere-step2").removeClass("active"); + $("#vsphere-step1").addClass("active"); + } + else if(CurStep == 3){ + CurStep = 2; + document.getElementById("host_name_div").style.display="none"; + document.getElementById("other_setting_div").style.display="none"; + document.getElementById("vc_setting_div").style.display="block"; + document.getElementById("vm_setting_div").style.display="none"; + $("#vsphere-step3").removeClass("active"); + $("#vsphere-step2").addClass("active"); + document.getElementById("vsphere_step_next").style.display="inline-block"; + document.getElementById("host_create_button").style.display="none"; + } +} +function NextClick(){ + if(CurStep == 1){ + var hostnameLen=document.getElementById("host_name").value.length; + if(hostnameLen > 0){ + CurStep = 2; + document.getElementById("host_name_div").style.display="none"; + document.getElementById("other_setting_div").style.display="none"; + document.getElementById("vc_setting_div").style.display="block"; + document.getElementById("vm_setting_div").style.display="none"; + $("#vsphere-step1").removeClass("active"); + $("#vsphere-step2").addClass("active"); + } + } + else if(CurStep == 2){ + var vcAderessLen=document.getElementById("vc_address").value.length; + var vcUserLen=document.getElementById("vc_user").value.length; + var vcPassLen=document.getElementById("vc_password").value.length; + var vcNetworkLen=document.getElementById("vc_network").value.length; + var datastoreLen=document.getElementById("datastore").value.length; + var datacenterLen=document.getElementById("datacenter").value.length; + var clusterLen=document.getElementById("cluster").value.length; + if(vcAderessLen && vcUserLen && vcPassLen && vcNetworkLen && datastoreLen && datacenterLen && clusterLen){ + CurStep = 3; + document.getElementById("host_name_div").style.display="none"; + document.getElementById("other_setting_div").style.display="none"; + document.getElementById("vc_setting_div").style.display="none"; + document.getElementById("vm_setting_div").style.display="block"; + $("#vsphere-step2").removeClass("active"); + $("#vsphere-step3").addClass("active"); + document.getElementById("vsphere_step_next").style.display="none"; + document.getElementById("host_create_button").style.display="inline-block"; + } + } +} + diff --git a/src/themes/basic/templates/layout.html b/src/themes/basic/templates/layout.html index 9f2b64021..43f009667 100644 --- a/src/themes/basic/templates/layout.html +++ b/src/themes/basic/templates/layout.html @@ -28,6 +28,7 @@ + {% block custom_css %} {% endblock %} @@ -109,23 +110,116 @@