def associate_tag(self, stub_config, name, tags): """Associates tags with specific VM.""" if not name or not tags: raise VmCLIException( 'Arguments name or tags are missing, cannot continue!') vm = self.get_vm_obj(name, fail_missing=True) # Get vmware ID representation in form 'vm-XXX' for later association vm_id = vm._GetMoId() vm_dynid = DynamicID(type='VirtualMachine', id=vm_id) # Create API services for Tag and TagAssociation backends tag_svc = Tag(stub_config) tag_asoc = TagAssociation(stub_config) # Search for tag object(s) tags_found = [] if ',' in tags: tags = tags.split(',') else: tags = [tags] for t in tag_svc.list(): tag = tag_svc.get(t) if tag.name in tags: tags_found.append(tag) if len(tags_found) != len(tags): raise VmCLIException('One or more tags were not found') # Asosociate tags with VM for tag in tags_found: tag_asoc.attach(tag_id=tag.id, object_id=vm_dynid) self.logger.info('All tags have been attached to the VM')
def get_obj(self, vimtype, name, default=False): """Gets the vsphere object associated with a given text name. If default is set to True and name does not match, return first object found.""" vimtype = VMWARE_TYPES.get(vimtype, None) if not vimtype: raise VmCLIException( 'Provided type does not match any existing VMware object types!' ) container = self.content.viewManager.CreateContainerView( self.content.rootFolder, [vimtype], True) if name is not None: for item in container.view: if item.name == name: return item # If searched object is not found and default is True, provide first instance found if default: # Order result list before selecting default list = builtins.list view_items = list({x.name: x for x in container.view}.items()) sorted_view = OrderedDict(sorted(view_items, key=lambda t: t[0])) sorted_view = list(sorted_view.values()) try: return sorted_view[0] except IndexError: return None return None
def register(name, class_name): """Registers class itself as a subcommand with a provided name.""" global COMMANDS if COMMANDS.get(name, None): raise VmCLIException( 'Subcommand with the name {} already registered!'.format(name)) else: COMMANDS[name] = class_name
def create_snapshot(self, vm, snapshot, desc, memory, quiesce): """Creates new snapshot on the VM.""" if desc is None: raise VmCLIException('Argument --desc is required with "create" operation!') self.logger.info('Creating snapshot of the virtual machine...') task = vm.CreateSnapshot_Task(name=snapshot, description=desc, memory=memory, quiesce=quiesce) self.wait_for_tasks([task])
def execute(self, args): if args.mem or args.cpu: self.change_hw_resource(args.name, args.mem, args.cpu) elif args.net: self.change_network(args.name, args.net, args.dev) elif args.vHWversion: self.change_vHWversion(args.name, args.vHWversion) else: raise VmCLIException('Too few arguments. Aborting...')
def normalize_memory(value): """Function converts passed value to integer, which will represent size in megabytes as well as performs control whether the value sits between global limits.""" value = convert_to_mb(value) if value < VM_MIN_MEM or value > VM_MAX_MEM: raise VmCLIException('Memory must be between {}-{} megabytes'.format(VM_MIN_MEM, VM_MAX_MEM)) else: return value
def change_network(self, name, net, dev): """Changes network associated with a specifc VM's network interface.""" vm = self.get_vm_obj(name, fail_missing=True) # locate network, which should be assigned to device network = self.get_obj('network', net) if not network: raise VmCLIException('Unable to find provided network {}! Aborting...'.format(net)) # search for Ethernet devices self.logger.info('Searching for ethernet devices attached to vm...') nic_counter = 1 for device in vm.config.hardware.device: # Search for a specific network interfaces if isinstance(device, vim.vm.device.VirtualEthernetCard): if nic_counter != dev: nic_counter += 1 continue if isinstance(network, vim.dvs.DistributedVirtualPortgroup): # specify backing that connects device to a DVS switch portgroup dvs_port_conn = vim.dvs.PortConnection( portgroupKey=network.key, switchUuid=network.config.distributedVirtualSwitch.uuid) backing = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo(port=dvs_port_conn) else: # expect simple vim.Network if DistributedVirtualPortgroup was not used backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo( useAutoDetect=False, network=network, deviceName=net) device.backing = backing # specify power status for nic device.connectable = vim.vm.device.VirtualDevice.ConnectInfo( connected=True, startConnected=True, allowGuestControl=True) # build object with change specifications nicspec = vim.vm.device.VirtualDeviceSpec(device=device) nicspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit config_spec = vim.vm.ConfigSpec(deviceChange=[nicspec]) self.logger.info("Attaching network {} to {}. network device on VM...".format(net, dev)) task = vm.ReconfigVM_Task(config_spec) self.wait_for_tasks([task]) return raise VmCLIException('Unable to find ethernet device on a specified target!')
def execute(self, args): """Creates empty vm without disk.""" if not (args.name or args.folder or args.resource_pool or args.datastore): raise VmCLIException( 'Missing arguments! Make sure name, folder, resource_pool and datastore are present.' ) datastore = self.get_obj('datastore', args.datastore) if not datastore: raise VmCLIException( 'Specified datastore not found! Datastore clusters are not supported with this operation. Aborting...' ) # Store logs and snapshots withing same directory as ds_path, which is [datastore]vm_name ds_path = '[{}]{}'.format(args.datastore, args.name) vm_files = vim.vm.FileInfo(logDirectory=None, snapshotDirectory=None, suspendDirectory=None, vmPathName=ds_path) # Load cpu and memory configuration if not args.mem: args.mem = c.VM_MIN_MEM args.mem = normalize_memory(args.mem) if not args.cpu: args.cpu = c.VM_MIN_CPU elif args.cpu < c.VM_MIN_CPU or args.cpu > c.VM_MAX_CPU: raise VmCLIException('CPU count must be between {}-{}'.format( c.VM_MIN_CPU, c.VM_MAX_CPU)) # configuration specification for the new vm, if no mem and cpu is provided, minimal values will be used config_spec = vim.vm.ConfigSpec() config_spec.name = args.name config_spec.memoryMB = args.mem config_spec.numCPUs = args.cpu config_spec.files = vm_files config_spec.guestId = 'otherLinux64Guest' folder = self.get_obj('folder', args.folder) resource_pool = self.get_obj('resource_pool', args.resource_pool) task = folder.CreateVM_Task(config=config_spec, pool=resource_pool) self.wait_for_tasks([task])
def automationSDKConnect(vcenter=None, username=None, password=None, insecure=None): """Creates stub_config with connection object for advanced features like VM Tagging present in vsphere-automation-sdk-python library, which is required to be installed: https://github.com/vmware/vsphere-automation-sdk-python""" vcenter = vcenter or conf.VCENTER username = username or conf.USERNAME password = password or conf.PASSWORD insecure = insecure or conf.INSECURE_CONNECTION if not HAS_AUTOMAT_SDK_INSTALLED: raise VmCLIException( 'Required vsphere-automation-sdk-python not installed. Exiting...') sys.exit(1) if (vcenter and username) and not password: password = getpass.getpass() elif not (vcenter and username and password): logger.error('No authentication credentials provided!') sys.exit(1) if not vcenter.startswith('http'): vcenter = 'https://{}/api'.format(vcenter) session = requests.Session() if insecure: requests.packages.urllib3.disable_warnings( requests.packages.urllib3.exceptions.InsecureRequestWarning) session.verify = False connector = get_requests_connector(session=session, url=vcenter) stub_config = StubConfigurationFactory.new_std_configuration(connector) # Pass user credentials (user/password) in the security context to authenticate. # login to vAPI endpoint user_password_security_context = create_user_password_security_context( username, password) stub_config.connector.set_security_context(user_password_security_context) # Create the stub for the session service and login by creating a session. session_svc = Session(stub_config) try: session_id = session_svc.create() except Unauthenticated: logger.error('Unable to connect. Check your credentials!') sys.exit(1) # Successful authentication. Store the session identifier in the security # context of the stub and use that for all subsequent remote requests session_security_context = create_session_security_context(session_id) stub_config.connector.set_security_context(session_security_context) return stub_config
def change_vHWversion(self, name, vHWversion=None): """Changes VM HW version. If version is None, then VM is set to the latest version.""" vm = self.get_vm_obj(name, fail_missing=True) if vHWversion == 'latest': version = None # None will default to latest so we don't need to search for it else: try: version = 'vmx-{:02d}'.format(vHWversion) except ValueError: raise VmCLIException('VM version must be integer or \'latest\'! Aborting...') if vm.runtime.powerState != 'poweredOff': raise VmCLIException('VM hardware version change cannot be performed on running VM! Aborting...') self.logger.info('Updating VM hardware version...') try: task = vm.UpgradeVM_Task(version=version) self.wait_for_tasks([task]) except vim.fault.AlreadyUpgraded: pass
def get_vm_obj(self, name, fail_missing=False): """Checks if passed object is of vim.VirtualMachine type, if not retrieves it from container view""" if isinstance(name, vim.VirtualMachine): return name else: self.logger.info('Loading required VMware resources...') vm = self.get_obj('vm', name) if not vm and fail_missing: raise VmCLIException( 'Unable to find specified VM {}! Aborting...'.format(name)) return vm
def change_hw_resource(self, name, mem=None, cpu=None): """Changes hardware resource of a specific VM.""" if not mem and not cpu: raise VmCLIException('Neither memory or cpu specified! Cannot run hardware reconfiguration.') vm = self.get_vm_obj(name, fail_missing=True) config_spec = vim.vm.ConfigSpec() if mem: mem = normalize_memory(mem) self.logger.info("Increasing memory to {} megabytes...".format(mem)) config_spec.memoryMB = mem if cpu: if cpu < c.VM_MIN_CPU or cpu > c.VM_MAX_CPU: raise VmCLIException('CPU count must be between {}-{}'.format(c.VM_MIN_CPU, c.VM_MAX_CPU)) else: self.logger.info("Increasing cpu count to {} cores...".format(cpu)) config_spec.numCPUs = cpu task = vm.ReconfigVM_Task(config_spec) self.wait_for_tasks([task])
def exec_inside_vm(self, name, commands, guest_user=None, guest_pass=None, wait_for_tools=False): """Runs provided command inside guest's operating system.""" if not commands: raise VmCLIException('No command provided for execution!') vm = self.get_vm_obj(name, fail_missing=True) self.logger.info("Checking if guest's OS has vmtools installed ...") if wait_for_tools: self.wait_for_guest_vmtools(vm) if vm.guest.toolsStatus in ['toolsNotInstalled', 'toolsNotRunning']: raise VmCLIException( "Guest's VMware tools are not installed or not running. Aborting..." ) try: credentials = vim.vm.guest.NamePasswordAuthentication( username=guest_user, password=guest_pass) for cmd in commands: executable = cmd.split()[0].lstrip() arguments = ' '.join(cmd.split()[1:]) try: self.logger.info( 'Running command "{} {}" inside guest'.format( executable, arguments)) progspec = vim.vm.guest.ProcessManager.ProgramSpec( programPath=executable, arguments=arguments) self.content.guestOperationsManager.processManager.StartProgramInGuest( vm, credentials, progspec) except vim.fault.FileNotFound as e: raise VmCLIException( e.msg + '. Try providing absolute path to the binary.') except vim.fault.InvalidGuestLogin as e: raise VmCLIException(e.msg)
def execute(self, args): if not HAS_AUTOMAT_SDK_INSTALLED: raise VmCLIException( 'Required vsphere-automation-sdk-python not installed. Exiting...' ) sys.exit(1) stub_config = automationSDKConnect(args.vcenter, args.username, args.password, args.insecure) if args.name: self.associate_tag(stub_config, args.name, args.tags) else: self.print_tags(stub_config)
def execute(self, args): if args.operation != 'list' and not args.snapshot: raise VmCLIException('Argument --snapshot is required with "{}" operation!'.format(args.operation)) vm = self.get_vm_obj(args.name, fail_missing=True) if args.operation == 'list': self.list_snapshots(vm) elif args.operation == 'create': self.create_snapshot(vm, args.snapshot, args.desc, args.memory, args.quiesce) elif args.operation == 'delete': self.delete_snapshot(vm, args.snapshot) elif args.operation == 'revert': self.revert_snapshot(vm, args.snapshot)
def convert_to_mb(value): """Converts size to megabytes.""" if isinstance(value, str): if value.endswith('T'): value = int(value.strip('T')) * 1024 * 1024 elif value.endswith('G'): value = int(value.strip('G')) * 1024 elif value.endswith('M'): value = int(value.strip('M')) elif value.endswith('K'): value = int(value.strip('K')) / 1024 # Assume bytes otherwise else: value = int(value) / 1024 / 1024 try: value = int(value) except ValueError: raise VmCLIException('Unable to convert memory size to gigabytes. Aborting...') return value
def attach_net_adapter(self, name, net): """Attaches virtual network adapter to the vm associated with a VLAN passed via argument.""" vm = self.get_vm_obj(name, fail_missing=True) # locate network, which should be assigned to device network = self.get_obj('network', net) if not network: raise VmCLIException( 'Unable to find provided network {}! Aborting...'.format(net)) # build virtual device device = vim.vm.device.VirtualVmxnet3(deviceInfo=vim.Description()) if isinstance(network, vim.dvs.DistributedVirtualPortgroup): # specify backing that connects device to a DVS switch portgroup dvs_port_conn = vim.dvs.PortConnection( portgroupKey=network.key, switchUuid=network.config.distributedVirtualSwitch.uuid) backing = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo( port=dvs_port_conn) else: # expect simple vim.Network if DistributedVirtualPortgroup was not used backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo( useAutoDetect=False, network=network, deviceName=net) device.backing = backing # specify power status for nic device.connectable = vim.vm.device.VirtualDevice.ConnectInfo( connected=False, startConnected=True, allowGuestControl=True) # build object with change specifications nicspec = vim.vm.device.VirtualDeviceSpec(device=device) nicspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add config_spec = vim.vm.ConfigSpec(deviceChange=[nicspec]) self.logger.info( 'Attaching network device to the virtual machine {}...'.format( name)) task = vm.ReconfigVM_Task(config_spec) self.wait_for_tasks([task])
def exec_callbacks(self, args, callback_args): """Runs any executable present inside project/callbacks/ directory on host with provided arguments. First argument to executable is always JSON object containing all arguments passed to vmcli and its subcommands via cli. Following are arguments passed as a value via command line argument callback. For example, this --callback 'var1; var2; multi word var' will be passed as: ./callbacks/script.sh '{"name": "..", "template": ...}' 'var1' 'var2' 'multi word var' """ # Parse additional callback arguments passed from command line if callback_args: callback_args = [ x.lstrip() for x in callback_args.rstrip(';').split(';') ] else: callback_args = [] # Get all callback scripts callbacks_dir = sorted(os.listdir('callbacks/')) callbacks = [ os.path.realpath('callbacks/' + x) for x in callbacks_dir if not x.startswith('.') ] # Prepare JSON serializable object from args namespace arguments = {} for argument in [x for x in dir(args) if not x.startswith('_')]: arguments[argument] = getattr(args, argument, None) arguments = json.dumps(arguments) for executable in callbacks: self.logger.info('Running callback "{}" ...'.format(executable)) command = [executable, arguments] command.extend(callback_args) try: subprocess.Popen(command).communicate() except OSError: raise VmCLIException( 'Unable to execute callback {}! Check it for errors'. format(executable))
def attach_hdd(self, name, size): """Attaches disk to a virtual machine. If no SCSI controller is present, then it is attached as well.""" if not size or size < VM_MIN_HDD or size > VM_MAX_HDD: raise VmCLIException('Hdd size must be between {}-{}'.format( VM_MIN_HDD, VM_MAX_HDD)) vm = self.get_vm_obj(name, fail_missing=True) disks = [] controller = None # iterate over existing devices and try to find disks and controllerKey self.logger.info( 'Searching for already existing disks and SCSI controllers...') for device in vm.config.hardware.device: # search for existing SCSI controller or create one if none found # TODO: provide flag when to create new controller if isinstance( device, vim.vm.device.VirtualSCSIController) and not controller: controller = device elif isinstance(device, vim.vm.device.VirtualDisk): disks.append(device) disk_unit_number = 0 controller_unit_number = 7 scsispec = None # if controller exists, calculate next unit number for disks otherwise create new controller and use defaults if controller: self.logger.info( 'Using existing SCSI controller(id:{}) to attach disk'.format( controller.key)) controller_unit_number = int(controller.key) for disk in disks: if disk.controllerKey == controller.key and disk_unit_number <= int( device.unitNumber): disk_unit_number = int(device.unitNumber) + 1 else: self.logger.info( 'No existing SCSI controller found. Creating new one...') scsispec = vim.vm.device.VirtualDeviceSpec() scsispec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add scsispec.device = vim.vm.device.ParaVirtualSCSIController( deviceInfo=vim.Description()) scsispec.device.slotInfo = vim.vm.device.VirtualDevice.PciBusSlotInfo( ) # if there is no controller on the device present, assign it default values scsispec.device.controllerKey = 100 scsispec.device.unitNumber = 3 scsispec.device.busNumber = 0 scsispec.device.hotAddRemove = True scsispec.device.sharedBus = 'noSharing' scsispec.device.scsiCtlrUnitNumber = controller_unit_number controller = scsispec.device controller.key = 100 if disk_unit_number >= 16: raise VmCLIException( 'The SCSI controller does not support any more disks!') elif disk_unit_number == 7: disk_unit_number = +1 # 7 is reserved for SCSI controller itself self.logger.info('Creating new empty disk with size {}G'.format(size)) diskspec = vim.vm.device.VirtualDeviceSpec() diskspec.fileOperation = "create" diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add diskspec.device = vim.vm.device.VirtualDisk() diskspec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo( ) diskspec.device.backing.diskMode = 'persistent' diskspec.device.backing.thinProvisioned = True diskspec.device.unitNumber = disk_unit_number diskspec.device.capacityInBytes = size * 1024 * 1024 * 1024 diskspec.device.capacityInKB = size * 1024 * 1024 diskspec.device.controllerKey = controller.key if scsispec: dev_change = [scsispec, diskspec] else: dev_change = [diskspec] config_spec = vim.vm.ConfigSpec(deviceChange=dev_change) self.logger.info('Attaching device to the virtual machine...') task = vm.ReconfigVM_Task(config_spec) self.wait_for_tasks([task])
def execute(self, args): """Clones VM, assigns it proper hardware devices, powers it on ad prepares it for further configuration.""" if not args.name or not args.template: raise VmCLIException( 'Arguments name or template are missing, cannot continue!') clone = CloneCommands(self.connection) clone.clone_vm(args.name, args.template, args.datacenter, args.folder, args.datastore, args.cluster, args.resource_pool, False, args.mem, args.cpu, flavor=args.flavor) vm = self.get_vm_obj(args.name, fail_missing=True) modify = ModifyCommands(self.connection) # Upgrade VM hardware version to the latest modify.change_vHWversion(vm, vHWversion='latest') # Change network assigned to the first interface on the VM if args.net: modify.change_network(vm, args.net, dev=1) if args.hdd: # Attach additional hard drive attach = AttachCommands(self.connection) attach.attach_hdd(vm, args.hdd) power = PowerCommands(self.connection) power.poweron_vm(vm) self.wait_for_guest_os(vm) if args.tags: args_copy = copy.deepcopy(args) args_copy.name = vm tag_cmd = TagCommands(self.connection) tag_cmd.execute(args_copy) execute = ExecCommands(self.connection) # Configure first ethernet device on the host, assumes traditional naming scheme if args.net_cfg: # assume prefix 24 if user forgots if len(args.net_cfg.split('/')) == 1: args.net_cfg += '/24' try: ip = netaddr.IPNetwork(args.net_cfg) gateway = list(ip)[1] except netaddr.core.AddrFormatError as e: ip, gateway = None, None self.logger.warning( str(e.message) + '. Skipping network configuration') if ip and gateway: # expects script inside template commands = [ '/bin/bash /usr/share/vmcli/provision-interfaces.sh {} {} {} {} {}' .format(ip.ip, ip.netmask, gateway, ip.network, ip.broadcast) ] execute.exec_inside_vm(vm, commands, args.guest_user, args.guest_pass, wait_for_tools=True) if conf.VM_ADDITIONAL_CMDS: execute.exec_inside_vm(vm, conf.VM_ADDITIONAL_CMDS, args.guest_user, args.guest_pass, wait_for_tools=True) self.logger.info('Deployed vm {}'.format(args.name)) # Execute callbacks from callbacks/ directory if args.callback: execute.exec_callbacks(args, args.callback)