Deploying the vSAN Witness Appliance

Deploying the vSAN Witness Appliance is an alternative to using a physical host to serve as the witness host in your stretched cluster configuration. Unlike a physical host, the appliance does not require a dedicated license or physical disks to store vSAN data.

You can download the vSAN Witness Appliance from the VMware website as a standard OVA file. Then you can install it by using the vSphere Client just like any other OVA file.

You can also upload the vSAN Witness Appliance OVA file through a script. Start with uploading each of the consisting OVA files separately in vCenter Server. First, create a function that uploads a single file in vCenter Server.

def uploadFile(srcURL, dstURL, create, lease, minProgress, progressIncrement, vmName=None, log=None):


    '''
    This function will upload vmdk file to vc by using http protocol
    @param srcURL: source url
    @param dstURL: destnate url
    @param create: http request method
    @param lease: HttpNfcLease object
    @param minProgress: file upload progress initial value
    @param progressIncrement: file upload progress update value
    @param vmName: imported virtual machine name
    @param log: log object @return:
    '''
srcData = urllib2.urlopen(srcURL)
length = int(srcData.headers['content-length'])
ssl._create_default_https_context = ssl._create_unverified_context
protocol, hostPort, reqStr = splitURL(dstURL)
dstHttpConn = createHttpConn(protocol, hostPort)
reqType = create and 'PUT' or 'POST'
dstHttpConn.putrequest(reqType, reqStr)
dstHttpConn.putheader('Content-Length', length)
dstHttpConn.endheaders()
bufSize = 1048768  # 1 MB
total = 0
progress = minProgress
if log:
# If args.log is available, then log to it
log = log.info
else
log = sys.stdout.write
log("%s: %s: Start: srcURL=%s dstURL=%s\n" % (time.asctime(time.localtime()), vmName, srcURL,
                                              dstURL))
log("%s: %s: progress=%d total=%d length=%d\n" % (time.asctime(time.localtime()), vmName, progress,
                                                  total, length))
while True:
    data = srcData.read(bufSize)
if lease.state != vim.HttpNfcLease.State.ready:
    break
dstHttpConn.send(data)
total = total + len(data)
progress = (int)(total * (progressIncrement) / length)
progress += minProgress
lease.Progress(progress)
if len(data) == 0:
    break
log("%s: %s: Finished: srcURL=%s dstURL=%s\n" % (time.asctime(time.localtime()), vmName, srcURL,
                                                 dstURL))
log("%s: %s: progress=%d total=%d length=%d\n" % \ (time.asctime(time.localtime()), vmName,
                                                    progress, total, length))
log("%s: %s: Lease State: %s\n" % \
    (time.asctime(time.localtime()), vmName, lease.state))
if lease.state == vim.HttpNfcLease.State.error:
    raise lease.error
dstHttpConn.getresponse()
return progress

Once you have a function for deploying a single file, create another one for uploading multiple files.

def uploadFiles(fileItems, lease, ovfURL, vmName=None, log=None):
    '''
    Upload witness vm's vmdk files to vCenter Server by using the HTTP protocol
    @param fileItems: the source vmdks read from ovf file
    @param lease: Represents a lease on a VM or a vApp, which can be used to import or export disks for
    the entity
    @param ovfURL: witness vApp ovf url
    @param vmName: The name of witness vm @param log: @return:
    '''


uploadUrlMap = {}
for kv in lease.info.deviceUrl:
    uploadUrlMap[kv.importKey] = (kv.key, kv.url)
progress = 5
increment = (int)(90 / len(fileItems))
for file in fileItems:
    ovfDevId = file.deviceId
srcDiskURL = urlparse.urljoin(ovfURL, file.path)
(viDevId, url) = uploadUrlMap[ovfDevId]
if lease.state == vim.HttpNfcLease.State.error:
    raise lease.error
elif lease.state != vim.HttpNfcLease.State.ready:
    raise Exception("%s: file upload aborted, lease state=%s" % (vmName,
                                                                 lease.state))
progress = uploadFile(srcDiskURL, url, file.create, lease, progress, increment,
                      vmName, log)

The next step is to define and implement a function that configures the networking settings, the supplied password as a vApp option, and the placement of the appliance on a specific host or resource pool.

The DeployWitnessOVF function, defined in the following example, configures the networking settings, the supplied password as a vApp option, and the placement of the appliance on a specific host or resource pool. This function parses only the contents of the OVF and not the entire vSAN Witness Appliance OVA. Password is the only additional argument required by the vSAN Witness Appliance. You must extract the contents of the witness OVA file to a folder containing the OVF and other required files. The OVA file is a .tar archive, that you can extract by using a wide variety of tools.

"""
Deploying  witness VM to vCenter.
The import process consists of the following steps:
1>Creating the VMs and/or vApps that make up the entity.
2>Uploading the virtual disk contents.
@param ovfURL: ovf source url
@param si: Managed Object ServiceInstance
@param host: HostSystem on which the VM located
@param vmName: VM name
@param dsRef: Datastore on which the VM located
@param vmFolder: Folder to which the VM belong to
@param vmPassword: Password for the VM
@param network: Managed Object Network of the VM
@return: Witness VM entity
"""


def DeployWitnessOVF(ovfURL, si, host, vmName, dsRef, vmFolder, vmPassword=None, network=None):
    rp = host.parent.resourcePool
    params = vim.OvfManager.CreateImportSpecParams()
    params.entityName = vmName
    params.hostSystem = host
    params.diskProvisioning = 'thin'

    f = urllib.urlopen(ovfURL)
    ovfData = f.read()

    import xml.etree.ElementTree as ET

    params.networkMapping = []
    if vmPassword:
        params.propertyMapping = [vim.KeyValue(key='vsan.witness.root.passwd', value=vmPassword)]
    ovf_tree = ET.fromstring(ovfData)

    for nwt in ovf_tree.findall('NetworkSection/Network'):
        nm = vim.OvfManager.NetworkMapping()
        nm.name = nwt.attrib['name']
        if network != None:
            nm.network = network
        else:
            nm.network = host.parent.network[0]
        params.networkMapping.append(nm)

    res = si.content.ovfManager.CreateImportSpec(ovfDescriptor=ovfData,
                                                 resourcePool=rp, datastore=dsRef, cisp=params)
    if isinstance(res, vim.MethodFault):
        raise res
    if res.error and len(res.error) & gt; 0:
        raise res.error[0]
    if not res.importSpec:
        raise Exception("CreateImportSpec raised no errors, but importSpec is not set")

    lease = rp.ImportVApp(spec=res.importSpec, folder=vmFolder, host=host)
    while lease.state == vim.HttpNfcLease.State.initializing:
        time.sleep(1)

    if lease.state == vim.HttpNfcLease.State.error:
        raise lease.error

    # Upload files
    uploadUrlMap = {}
    for kv in lease.info.deviceUrl:
        uploadUrlMap[kv.importKey] = (kv.key, kv.url)

    progress = 5
    increment = (int)(90 / len(res.fileItem))
    for file in res.fileItem:
        ovfDevId = file.deviceId
        srcDiskURL = urlparse.urljoin(ovfURL, file.path)
        (viDevId, url) = uploadUrlMap[ovfDevId]
        if lease.state == vim.HttpNfcLease.State.error:
            raise lease.error
        elif lease.state != vim.HttpNfcLease.State.ready:
            raise Exception("%s: file upload aborted, lease state=%s" % \
                            (vmName, lease.state))
        srcData = urllib2.urlopen(srcDiskURL)
        length = int(srcData.headers['content-length'])
        result = urlparse.urlparse(url)
        protocol, hostPort, reqStr = result.scheme, result.netloc, result.path
        if protocol == 'https':
            dstHttpConn = httplib.HTTPSConnection(hostPort)
        else:
            dstHttpConn = httplib.HTTPConnection(hostPort)
        reqType = file.create and 'PUT' or 'POST'
        dstHttpConn.putrequest(reqType, reqStr)
        dstHttpConn.putheader('Content-Length', length)
        dstHttpConn.endheaders()

        bufSize = 1048768  # 1 MB
        total = 0
        currProgress = progress
        while True:
            data = srcData.read(bufSize)
            if lease.state != vim.HttpNfcLease.State.ready:
                break
            dstHttpConn.send(data)
            total = total + len(data)
            currProgress += (int)(total * (increment) / length)
            progress += minProgress
            lease.Progress(progress)
            if len(data) == 0:
                break
        if lease.state == vim.HttpNfcLease.State.error:
            raise lease.error

        dstHttpConn.getresponse()
        progress = currProgress
    lease.Complete()

    return lease.info.entity