mirror of https://github.com/Kodomo/esxi-vm
Add -o option, fix isSummary save, add beta esxi-vm-destroy
parent
625e4f269c
commit
5809a2df0b
|
@ -0,0 +1 @@
|
||||||
|
esxi_vm_functions.pyc
|
|
@ -0,0 +1,14 @@
|
||||||
|
all:
|
||||||
|
@echo "make install"
|
||||||
|
|
||||||
|
install:
|
||||||
|
install -m 755 ./esxi-vm-create /usr/local/bin/
|
||||||
|
install -m 755 ./esxi-vm-destroy /usr/local/bin/
|
||||||
|
install -m 755 ./esxi_vm_functions.py /usr/local/bin/
|
||||||
|
@echo "Install Success."
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -fr /usr/local/bin/esxi-vm-create
|
||||||
|
rm -fr /usr/local/bin/esxi-vm-destroy
|
||||||
|
rm -fr /usr/local/bin/esxi_vm_functions.py
|
||||||
|
rm -fr /usr/local/bin/esxi_vm_functions.pyc
|
24
README.md
24
README.md
|
@ -34,7 +34,7 @@ Usage
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
You must enable ssh access on your ESXi server. The VMware VIX API tools are not required.
|
You must enable ssh access on your ESXi server. Google 'how to enable ssh access on esxi' for instructions. The VMware VIX API tools are not required.
|
||||||
|
|
||||||
It's HIGHLY RECOMMENDED to use password-less authentication by copying your ssh public keys to the ESXi host, otherwise your ESXi root password could be stored in clear-text in your home directory.
|
It's HIGHLY RECOMMENDED to use password-less authentication by copying your ssh public keys to the ESXi host, otherwise your ESXi root password could be stored in clear-text in your home directory.
|
||||||
|
|
||||||
|
@ -50,9 +50,11 @@ Command Line Args
|
||||||
|
|
||||||
```
|
```
|
||||||
./esxi-vm-create --help
|
./esxi-vm-create --help
|
||||||
|
|
||||||
usage: esxi-vm-create [-h] [-d] [-H HOST] [-U USER] [-P PASSWORD] [-n NAME]
|
usage: esxi-vm-create [-h] [-d] [-H HOST] [-U USER] [-P PASSWORD] [-n NAME]
|
||||||
[-c CPU] [-m MEM] [-v HDISK] [-i ISO] [-N NET] [-M MAC]
|
[-c CPU] [-m MEM] [-v HDISK] [-i ISO] [-N NET] [-M MAC]
|
||||||
[-S STORE] [-g GUESTOS] [-V] [--summary] [-u]
|
[-S STORE] [-g GUESTOS] [-o VMXOPTS] [-V] [--summary]
|
||||||
|
[-u]
|
||||||
|
|
||||||
ESXi Create VM utility.
|
ESXi Create VM utility.
|
||||||
|
|
||||||
|
@ -65,20 +67,21 @@ optional arguments:
|
||||||
ESXi Host password (*****)
|
ESXi Host password (*****)
|
||||||
-n NAME, --name NAME VM name
|
-n NAME, --name NAME VM name
|
||||||
-c CPU, --cpu CPU Number of vCPUS (2)
|
-c CPU, --cpu CPU Number of vCPUS (2)
|
||||||
-m MEM, --mem MEM Memory in GB (2)
|
-m MEM, --mem MEM Memory in GB (4)
|
||||||
-v HDISK, --vdisk HDISK
|
-v HDISK, --vdisk HDISK
|
||||||
Size of virt hdisk (12)
|
Size of virt hdisk (20)
|
||||||
-i ISO, --iso ISO CDROM ISO Path | None (None)
|
-i ISO, --iso ISO CDROM ISO Path | None (None)
|
||||||
-N NET, --net NET Network Interface | None (192.168.1)
|
-N NET, --net NET Network Interface | None (None)
|
||||||
-M MAC, --mac MAC MAC address
|
-M MAC, --mac MAC MAC address
|
||||||
-S STORE, --store STORE
|
-S STORE, --store STORE
|
||||||
vmfs Store | LeastUsed (DS_3TB_m)
|
vmfs Store | LeastUsed (LeastUsed)
|
||||||
-g GUESTOS, --guestos GUESTOS
|
-g GUESTOS, --guestos GUESTOS
|
||||||
Guest OS. (centos-64)
|
Guest OS. (centos-64)
|
||||||
|
-o VMXOPTS, --options VMXOPTS
|
||||||
|
Comma list of VMX Options.
|
||||||
-V, --verbose Enable Verbose mode (False)
|
-V, --verbose Enable Verbose mode (False)
|
||||||
--summary Display Summary (False)
|
--summary Display Summary (False)
|
||||||
-u, --updateDefaults Update Default VM settings stored in ~/.esxi-vm.yml
|
-u, --updateDefaults Update Default VM settings stored in ~/.esxi-vm.yml
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -176,6 +179,12 @@ Guest OS: centos-64
|
||||||
MAC: 00:0c:29:ea:a0:42
|
MAC: 00:0c:29:ea:a0:42
|
||||||
00:0c:29:ea:a0:42
|
00:0c:29:ea:a0:42
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Merge/Add extra VMX options, saved as default.
|
||||||
|
```
|
||||||
|
./esxi-vm-create -o 'floppy0.present = "TRUE",svga.autodetect = "TRUE",svga.present = "TRUE"' -u
|
||||||
|
Saving new Defaults to ~/.esxi-vm.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
License
|
License
|
||||||
|
@ -202,4 +211,3 @@ Support
|
||||||
Website : http://www.jintegrate.co
|
Website : http://www.jintegrate.co
|
||||||
|
|
||||||
github : http://github.com/josenk/
|
github : http://github.com/josenk/
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ STORE = ConfigData['STORE']
|
||||||
NET = ConfigData['NET']
|
NET = ConfigData['NET']
|
||||||
ISO = ConfigData['ISO']
|
ISO = ConfigData['ISO']
|
||||||
GUESTOS = ConfigData['GUESTOS']
|
GUESTOS = ConfigData['GUESTOS']
|
||||||
|
VMXOPTS = ConfigData['VMXOPTS']
|
||||||
|
|
||||||
ErrorMessages = ""
|
ErrorMessages = ""
|
||||||
MAC = ""
|
MAC = ""
|
||||||
|
@ -60,6 +61,7 @@ parser.add_argument("-N", "--net", dest='NET', type=str, help="Network Interface
|
||||||
parser.add_argument("-M", "--mac", dest='MAC', type=str, help="MAC address")
|
parser.add_argument("-M", "--mac", dest='MAC', type=str, help="MAC address")
|
||||||
parser.add_argument("-S", "--store", dest='STORE', type=str, help="vmfs Store | LeastUsed (" + str(STORE) + ")")
|
parser.add_argument("-S", "--store", dest='STORE', type=str, help="vmfs Store | LeastUsed (" + str(STORE) + ")")
|
||||||
parser.add_argument("-g", "--guestos", dest='GUESTOS', type=str, help="Guest OS. (" + str(GUESTOS) + ")")
|
parser.add_argument("-g", "--guestos", dest='GUESTOS', type=str, help="Guest OS. (" + str(GUESTOS) + ")")
|
||||||
|
parser.add_argument("-o", "--options", dest='VMXOPTS', type=str, default='NIL', help="Comma list of VMX Options.")
|
||||||
parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")")
|
parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")")
|
||||||
parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")")
|
parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")")
|
||||||
parser.add_argument("-u", "--updateDefaults", dest='UPDATE', action='store_true', help="Update Default VM settings stored in ~/.esxi-vm.yml")
|
parser.add_argument("-u", "--updateDefaults", dest='UPDATE', action='store_true', help="Update Default VM settings stored in ~/.esxi-vm.yml")
|
||||||
|
@ -100,11 +102,17 @@ if STORE == "":
|
||||||
STORE = "LeastUsed"
|
STORE = "LeastUsed"
|
||||||
if args.GUESTOS:
|
if args.GUESTOS:
|
||||||
GUESTOS=args.GUESTOS
|
GUESTOS=args.GUESTOS
|
||||||
|
if args.VMXOPTS == '' and VMXOPTS != '':
|
||||||
|
VMXOPTS=''
|
||||||
|
if args.VMXOPTS and args.VMXOPTS != 'NIL':
|
||||||
|
VMXOPTS=args.VMXOPTS.split(",")
|
||||||
|
|
||||||
|
|
||||||
if args.UPDATE:
|
if args.UPDATE:
|
||||||
print "Saving new Defaults to ~/.esxi-vm.yml"
|
print "Saving new Defaults to ~/.esxi-vm.yml"
|
||||||
ConfigData['isDryRun'] = isDryRun
|
ConfigData['isDryRun'] = isDryRun
|
||||||
ConfigData['isVerbose'] = isVerbose
|
ConfigData['isVerbose'] = isVerbose
|
||||||
|
ConfigData['isSummary'] = isSummary
|
||||||
ConfigData['HOST'] = HOST
|
ConfigData['HOST'] = HOST
|
||||||
ConfigData['USER'] = USER
|
ConfigData['USER'] = USER
|
||||||
ConfigData['PASSWORD'] = PASSWORD
|
ConfigData['PASSWORD'] = PASSWORD
|
||||||
|
@ -117,6 +125,7 @@ if args.UPDATE:
|
||||||
ConfigData['NET'] = NET
|
ConfigData['NET'] = NET
|
||||||
ConfigData['ISO'] = ISO
|
ConfigData['ISO'] = ISO
|
||||||
ConfigData['GUESTOS'] = GUESTOS
|
ConfigData['GUESTOS'] = GUESTOS
|
||||||
|
ConfigData['VMXOPTS'] = VMXOPTS
|
||||||
SaveConfig(ConfigData)
|
SaveConfig(ConfigData)
|
||||||
if NAME == "":
|
if NAME == "":
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@ -353,6 +362,33 @@ if NET != "None":
|
||||||
VMX.append('ethernet0.addressType = "static"')
|
VMX.append('ethernet0.addressType = "static"')
|
||||||
VMX.append('ethernet0.address = "' + MAC + '"')
|
VMX.append('ethernet0.address = "' + MAC + '"')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Merge extra VMX options
|
||||||
|
for VMXopt in VMXOPTS:
|
||||||
|
try:
|
||||||
|
k,v = VMXopt.split("=")
|
||||||
|
except:
|
||||||
|
k=""
|
||||||
|
v=""
|
||||||
|
key = k.lstrip().strip()
|
||||||
|
value = v.lstrip().strip()
|
||||||
|
for i in VMX:
|
||||||
|
try:
|
||||||
|
ikey,ivalue = i.split("=")
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
if ikey.lstrip().strip().lower() == key.lower():
|
||||||
|
index = VMX.index(i)
|
||||||
|
VMX[index] = ikey + " = " + value
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if key != '' and value != '':
|
||||||
|
VMX.append(key + " = " + value)
|
||||||
|
|
||||||
|
if isVerbose and VMXOPTS != '':
|
||||||
|
print "VMX file:"
|
||||||
|
for i in VMX:
|
||||||
|
print i
|
||||||
|
|
||||||
MyVM = FullPath + "/" + NAME
|
MyVM = FullPath + "/" + NAME
|
||||||
if CheckHasErrors:
|
if CheckHasErrors:
|
||||||
|
@ -472,5 +508,3 @@ else:
|
||||||
else:
|
else:
|
||||||
print GeneratedMAC
|
print GeneratedMAC
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
|
||||||
|
import argparse # Argument parser
|
||||||
|
import datetime # For current Date/Time
|
||||||
|
import time
|
||||||
|
import os.path # To check if file exists
|
||||||
|
import sys # For args
|
||||||
|
import re # For regex
|
||||||
|
import paramiko # For remote ssh
|
||||||
|
import yaml
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from esxi_vm_functions import *
|
||||||
|
|
||||||
|
# Defaults and Variable setup
|
||||||
|
ConfigData = setup_config()
|
||||||
|
NAME = ""
|
||||||
|
LOG = ConfigData['LOG']
|
||||||
|
isDryRun = ConfigData['isDryRun']
|
||||||
|
isVerbose = ConfigData['isVerbose']
|
||||||
|
isSummary = ConfigData['isSummary']
|
||||||
|
HOST = ConfigData['HOST']
|
||||||
|
USER = ConfigData['USER']
|
||||||
|
PASSWORD = ConfigData['PASSWORD']
|
||||||
|
CPU = ConfigData['CPU']
|
||||||
|
MEM = ConfigData['MEM']
|
||||||
|
HDISK = int(ConfigData['HDISK'])
|
||||||
|
DISKFORMAT = ConfigData['DISKFORMAT']
|
||||||
|
VIRTDEV = ConfigData['VIRTDEV']
|
||||||
|
STORE = ConfigData['STORE']
|
||||||
|
NET = ConfigData['NET']
|
||||||
|
ISO = ConfigData['ISO']
|
||||||
|
GUESTOS = ConfigData['GUESTOS']
|
||||||
|
|
||||||
|
ErrorMessages = ""
|
||||||
|
CheckHasErrors = False
|
||||||
|
DSPATH=""
|
||||||
|
DSSTORE=""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Process Arguments
|
||||||
|
#
|
||||||
|
parser = argparse.ArgumentParser(description='ESXi Create VM utility.')
|
||||||
|
|
||||||
|
parser.add_argument("-H", "--Host", dest='HOST', type=str, help="ESXi Host/IP (" + str(HOST) + ")")
|
||||||
|
parser.add_argument("-U", "--User", dest='USER', type=str, help="ESXi Host username (" + str(USER) + ")")
|
||||||
|
parser.add_argument("-P", "--Password", dest='PASSWORD', type=str, help="ESXi Host password (*****)")
|
||||||
|
parser.add_argument("-n", "--name", dest='NAME', type=str, help="VM name")
|
||||||
|
parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")")
|
||||||
|
parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")")
|
||||||
|
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.isVerbosearg:
|
||||||
|
isVerbose = True
|
||||||
|
if args.isSummaryarg:
|
||||||
|
isSummary = True
|
||||||
|
if args.HOST:
|
||||||
|
HOST=args.HOST
|
||||||
|
if args.USER:
|
||||||
|
USER=args.USER
|
||||||
|
if args.PASSWORD:
|
||||||
|
PASSWORD=args.PASSWORD
|
||||||
|
if args.NAME:
|
||||||
|
NAME=args.NAME
|
||||||
|
|
||||||
|
#
|
||||||
|
# main()
|
||||||
|
#
|
||||||
|
LogOutput = '{'
|
||||||
|
LogOutput += '"datetime":"' + str(theCurrDateTime()) + '",'
|
||||||
|
|
||||||
|
if NAME == "":
|
||||||
|
print "ERROR: Missing required option --name"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(HOST, username=USER, password=PASSWORD)
|
||||||
|
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("esxcli system version get |grep Version")
|
||||||
|
type(stdin)
|
||||||
|
if re.match("Version", str(stdout.readlines())) is not None:
|
||||||
|
print "Unable to determine if this is a ESXi Host: %s, username: %s" % (HOST, USER)
|
||||||
|
sys.exit(1)
|
||||||
|
except:
|
||||||
|
print "The Error is " + str(sys.exc_info()[0])
|
||||||
|
print "Unable to access ESXi Host: %s, username: %s" % (HOST, USER)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Check if VM exists
|
||||||
|
#
|
||||||
|
VMID = -1
|
||||||
|
try:
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/getallvms")
|
||||||
|
type(stdin)
|
||||||
|
for line in stdout.readlines():
|
||||||
|
splitLine = line.split()
|
||||||
|
if NAME == splitLine[1]:
|
||||||
|
VMID = splitLine[0]
|
||||||
|
JNK = line.split('[')[1]
|
||||||
|
STORE = JNK.split(']')[0]
|
||||||
|
VMDIR = splitLine[3]
|
||||||
|
|
||||||
|
if VMID == -1:
|
||||||
|
print "Warning: VM " + NAME + " doesn't exists."
|
||||||
|
ErrorMessages += " VM " + NAME + " doesn't exists."
|
||||||
|
CheckHasErrors = True
|
||||||
|
CheckHasWarnings = True
|
||||||
|
except:
|
||||||
|
print "The Error is " + str(sys.exc_info()[0])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get List of Volumes,
|
||||||
|
try:
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("esxcli storage filesystem list |grep '/vmfs/volumes/.*true VMFS' |sort -nk7")
|
||||||
|
type(stdin)
|
||||||
|
VOLUMES = {}
|
||||||
|
for line in stdout.readlines():
|
||||||
|
splitLine = line.split()
|
||||||
|
VOLUMES[splitLine[0]] = splitLine[1]
|
||||||
|
except:
|
||||||
|
print "The Error is " + str(sys.exc_info()[0])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Convert STORE to path and visa-versa
|
||||||
|
V = []
|
||||||
|
for Path in VOLUMES:
|
||||||
|
V.append(VOLUMES[Path])
|
||||||
|
if STORE == Path or STORE == VOLUMES[Path]:
|
||||||
|
DSPATH = Path
|
||||||
|
DSSTORE = VOLUMES[Path]
|
||||||
|
|
||||||
|
|
||||||
|
if CheckHasErrors:
|
||||||
|
Result = "Errors"
|
||||||
|
else:
|
||||||
|
Result = "Success"
|
||||||
|
|
||||||
|
if not CheckHasErrors:
|
||||||
|
try:
|
||||||
|
|
||||||
|
CurrentState = ""
|
||||||
|
CurrentStateCounter = 0
|
||||||
|
while CurrentState != "off":
|
||||||
|
if isVerbose:
|
||||||
|
print "Get state VM"
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.getstate " + str(VMID))
|
||||||
|
type(stdin)
|
||||||
|
lines = str(stdout.readlines()) + str(stderr.readlines())
|
||||||
|
if isVerbose:
|
||||||
|
print "power.getstate: " + lines
|
||||||
|
if re.search("Powered off", lines):
|
||||||
|
CurrentState = "off"
|
||||||
|
|
||||||
|
# Power off VM
|
||||||
|
if isVerbose:
|
||||||
|
print "Power OFF VM"
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.off " + str(VMID) + " ||echo")
|
||||||
|
type(stdin)
|
||||||
|
lines = str(stdout.readlines()) + str(stderr.readlines())
|
||||||
|
if isVerbose:
|
||||||
|
print "power.off: " + str(lines)
|
||||||
|
|
||||||
|
CurrentStateCounter += 1
|
||||||
|
if CurrentStateCounter >10:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# destroy VM
|
||||||
|
if isVerbose:
|
||||||
|
print "Destroy VM"
|
||||||
|
(stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/destroy " + str(VMID))
|
||||||
|
type(stdin)
|
||||||
|
lines = str(stdout.readlines()) + str(stderr.readlines())
|
||||||
|
if isVerbose:
|
||||||
|
print "destroy: " + str(lines)
|
||||||
|
|
||||||
|
except:
|
||||||
|
print "There was an error destroying the VM."
|
||||||
|
ErrorMessages += " There was an error destroying the VM."
|
||||||
|
CheckHasErrors = True
|
||||||
|
Result = "Fail"
|
||||||
|
|
||||||
|
# Print Summary
|
||||||
|
|
||||||
|
#
|
||||||
|
# The output log string
|
||||||
|
LogOutput += '"Host":"' + HOST + '",'
|
||||||
|
LogOutput += '"Name":"' + NAME + '",'
|
||||||
|
LogOutput += '"Store Used":"' + DSPATH + '",'
|
||||||
|
LogOutput += '"Verbose":"' + str(isVerbose) + '",'
|
||||||
|
if ErrorMessages != "":
|
||||||
|
LogOutput += '"Error Message":"' + ErrorMessages + '",'
|
||||||
|
LogOutput += '"Result":"' + Result + '",'
|
||||||
|
LogOutput += '"Completion Time":"' + str(theCurrDateTime()) + '"'
|
||||||
|
LogOutput += '}\n'
|
||||||
|
try:
|
||||||
|
with open(LOG, "a+w") as FD:
|
||||||
|
FD.write(LogOutput)
|
||||||
|
except:
|
||||||
|
print "Error writing to log file: " + LOG
|
||||||
|
|
||||||
|
if isSummary:
|
||||||
|
if isVerbose:
|
||||||
|
print "ESXi Host: " + HOST
|
||||||
|
print "VM NAME: " + NAME
|
||||||
|
print "Path: " + DSSTORE
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if CheckHasErrors and not CheckHasWarnings:
|
||||||
|
print "Failed"
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print "Success"
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,10 @@ def setup_config():
|
||||||
ISO="None",
|
ISO="None",
|
||||||
|
|
||||||
# Default GuestOS type. (See VMware documentation for all available options)
|
# Default GuestOS type. (See VMware documentation for all available options)
|
||||||
GUESTOS="centos-64"
|
GUESTOS="centos-64",
|
||||||
|
|
||||||
|
# Extra VMX options
|
||||||
|
VMXOPTS=""
|
||||||
)
|
)
|
||||||
|
|
||||||
ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml"
|
ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml"
|
||||||
|
@ -112,4 +115,3 @@ def float2human(num):
|
||||||
return '0 bytes'
|
return '0 bytes'
|
||||||
if num == 1:
|
if num == 1:
|
||||||
return '1 byte'
|
return '1 byte'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue