1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2007, 2012 Red Hat, Inc 5# Michael DeHaan <michael.dehaan@gmail.com> 6# Seth Vidal <skvidal@fedoraproject.org> 7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 8 9from __future__ import absolute_import, division, print_function 10__metaclass__ = type 11 12 13DOCUMENTATION = ''' 14--- 15module: virt 16short_description: Manages virtual machines supported by libvirt 17description: 18 - Manages virtual machines supported by I(libvirt). 19options: 20 name: 21 description: 22 - name of the guest VM being managed. Note that VM must be previously 23 defined with xml. 24 - This option is required unless I(command) is C(list_vms) or C(info). 25 type: str 26 aliases: 27 - guest 28 state: 29 description: 30 - Note that there may be some lag for state requests like C(shutdown) 31 since these refer only to VM states. After starting a guest, it may not 32 be immediately accessible. 33 state and command are mutually exclusive except when command=list_vms. In 34 this case all VMs in specified state will be listed. 35 choices: [ destroyed, paused, running, shutdown ] 36 type: str 37 command: 38 description: 39 - In addition to state management, various non-idempotent commands are available. 40 choices: [ create, define, destroy, freemem, get_xml, info, list_vms, nodeinfo, pause, shutdown, start, status, stop, undefine, unpause, virttype ] 41 type: str 42 autostart: 43 description: 44 - start VM at host startup. 45 type: bool 46 uri: 47 description: 48 - libvirt connection uri. 49 default: qemu:///system 50 type: str 51 xml: 52 description: 53 - XML document used with the define command. 54 - Must be raw XML content using C(lookup). XML cannot be reference to a file. 55 type: str 56requirements: 57 - python >= 2.6 58 - libvirt-python 59author: 60 - Ansible Core Team 61 - Michael DeHaan 62 - Seth Vidal (@skvidal) 63''' 64 65EXAMPLES = ''' 66# a playbook task line: 67- community.libvirt.virt: 68 name: alpha 69 state: running 70 71# /usr/bin/ansible invocations 72# ansible host -m virt -a "name=alpha command=status" 73# ansible host -m virt -a "name=alpha command=get_xml" 74# ansible host -m virt -a "name=alpha command=create uri=lxc:///" 75 76# defining and launching an LXC guest 77- name: define vm 78 community.libvirt.virt: 79 command: define 80 xml: "{{ lookup('template', 'container-template.xml.j2') }}" 81 uri: 'lxc:///' 82- name: start vm 83 community.libvirt.virt: 84 name: foo 85 state: running 86 uri: 'lxc:///' 87 88# setting autostart on a qemu VM (default uri) 89- name: set autostart for a VM 90 community.libvirt.virt: 91 name: foo 92 autostart: yes 93 94# Defining a VM and making is autostart with host. VM will be off after this task 95- name: define vm from xml and set autostart 96 community.libvirt.virt: 97 command: define 98 xml: "{{ lookup('template', 'vm_template.xml.j2') }}" 99 autostart: yes 100 101# Listing VMs 102- name: list all VMs 103 community.libvirt.virt: 104 command: list_vms 105 register: all_vms 106 107- name: list only running VMs 108 community.libvirt.virt: 109 command: list_vms 110 state: running 111 register: running_vms 112''' 113 114RETURN = ''' 115# for list_vms command 116list_vms: 117 description: The list of vms defined on the remote system 118 type: list 119 returned: success 120 sample: [ 121 "build.example.org", 122 "dev.example.org" 123 ] 124# for status command 125status: 126 description: The status of the VM, among running, crashed, paused and shutdown 127 type: str 128 sample: "success" 129 returned: success 130''' 131 132import traceback 133 134try: 135 import libvirt 136 from libvirt import libvirtError 137except ImportError: 138 HAS_VIRT = False 139else: 140 HAS_VIRT = True 141 142import re 143 144from ansible.module_utils.basic import AnsibleModule 145from ansible.module_utils._text import to_native 146 147 148VIRT_FAILED = 1 149VIRT_SUCCESS = 0 150VIRT_UNAVAILABLE = 2 151 152ALL_COMMANDS = [] 153VM_COMMANDS = ['create', 'define', 'destroy', 'get_xml', 'pause', 'shutdown', 'status', 'start', 'stop', 'undefine', 'unpause'] 154HOST_COMMANDS = ['freemem', 'info', 'list_vms', 'nodeinfo', 'virttype'] 155ALL_COMMANDS.extend(VM_COMMANDS) 156ALL_COMMANDS.extend(HOST_COMMANDS) 157 158VIRT_STATE_NAME_MAP = { 159 0: 'running', 160 1: 'running', 161 2: 'running', 162 3: 'paused', 163 4: 'shutdown', 164 5: 'shutdown', 165 6: 'crashed', 166} 167 168 169class VMNotFound(Exception): 170 pass 171 172 173class LibvirtConnection(object): 174 175 def __init__(self, uri, module): 176 177 self.module = module 178 179 cmd = "uname -r" 180 rc, stdout, stderr = self.module.run_command(cmd) 181 182 if "xen" in stdout: 183 conn = libvirt.open(None) 184 elif "esx" in uri: 185 auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None] 186 conn = libvirt.openAuth(uri, auth) 187 else: 188 conn = libvirt.open(uri) 189 190 if not conn: 191 raise Exception("hypervisor connection failure") 192 193 self.conn = conn 194 195 def find_vm(self, vmid): 196 """ 197 Extra bonus feature: vmid = -1 returns a list of everything 198 """ 199 conn = self.conn 200 201 vms = [] 202 203 # this block of code borrowed from virt-manager: 204 # get working domain's name 205 ids = conn.listDomainsID() 206 for id in ids: 207 vm = conn.lookupByID(id) 208 vms.append(vm) 209 # get defined domain 210 names = conn.listDefinedDomains() 211 for name in names: 212 vm = conn.lookupByName(name) 213 vms.append(vm) 214 215 if vmid == -1: 216 return vms 217 218 for vm in vms: 219 if vm.name() == vmid: 220 return vm 221 222 raise VMNotFound("virtual machine %s not found" % vmid) 223 224 def shutdown(self, vmid): 225 return self.find_vm(vmid).shutdown() 226 227 def pause(self, vmid): 228 return self.suspend(vmid) 229 230 def unpause(self, vmid): 231 return self.resume(vmid) 232 233 def suspend(self, vmid): 234 return self.find_vm(vmid).suspend() 235 236 def resume(self, vmid): 237 return self.find_vm(vmid).resume() 238 239 def create(self, vmid): 240 return self.find_vm(vmid).create() 241 242 def destroy(self, vmid): 243 return self.find_vm(vmid).destroy() 244 245 def undefine(self, vmid): 246 return self.find_vm(vmid).undefine() 247 248 def get_status2(self, vm): 249 state = vm.info()[0] 250 return VIRT_STATE_NAME_MAP.get(state, "unknown") 251 252 def get_status(self, vmid): 253 state = self.find_vm(vmid).info()[0] 254 return VIRT_STATE_NAME_MAP.get(state, "unknown") 255 256 def nodeinfo(self): 257 return self.conn.getInfo() 258 259 def get_type(self): 260 return self.conn.getType() 261 262 def get_xml(self, vmid): 263 vm = self.conn.lookupByName(vmid) 264 return vm.XMLDesc(0) 265 266 def get_maxVcpus(self, vmid): 267 vm = self.conn.lookupByName(vmid) 268 return vm.maxVcpus() 269 270 def get_maxMemory(self, vmid): 271 vm = self.conn.lookupByName(vmid) 272 return vm.maxMemory() 273 274 def getFreeMemory(self): 275 return self.conn.getFreeMemory() 276 277 def get_autostart(self, vmid): 278 vm = self.conn.lookupByName(vmid) 279 return vm.autostart() 280 281 def set_autostart(self, vmid, val): 282 vm = self.conn.lookupByName(vmid) 283 return vm.setAutostart(val) 284 285 def define_from_xml(self, xml): 286 return self.conn.defineXML(xml) 287 288 289class Virt(object): 290 291 def __init__(self, uri, module): 292 self.module = module 293 self.uri = uri 294 295 def __get_conn(self): 296 self.conn = LibvirtConnection(self.uri, self.module) 297 return self.conn 298 299 def get_vm(self, vmid): 300 self.__get_conn() 301 return self.conn.find_vm(vmid) 302 303 def state(self): 304 vms = self.list_vms() 305 state = [] 306 for vm in vms: 307 state_blurb = self.conn.get_status(vm) 308 state.append("%s %s" % (vm, state_blurb)) 309 return state 310 311 def info(self): 312 vms = self.list_vms() 313 info = dict() 314 for vm in vms: 315 data = self.conn.find_vm(vm).info() 316 # libvirt returns maxMem, memory, and cpuTime as long()'s, which 317 # xmlrpclib tries to convert to regular int's during serialization. 318 # This throws exceptions, so convert them to strings here and 319 # assume the other end of the xmlrpc connection can figure things 320 # out or doesn't care. 321 info[vm] = dict( 322 state=VIRT_STATE_NAME_MAP.get(data[0], "unknown"), 323 maxMem=str(data[1]), 324 memory=str(data[2]), 325 nrVirtCpu=data[3], 326 cpuTime=str(data[4]), 327 autostart=self.conn.get_autostart(vm), 328 ) 329 330 return info 331 332 def nodeinfo(self): 333 self.__get_conn() 334 data = self.conn.nodeinfo() 335 info = dict( 336 cpumodel=str(data[0]), 337 phymemory=str(data[1]), 338 cpus=str(data[2]), 339 cpumhz=str(data[3]), 340 numanodes=str(data[4]), 341 sockets=str(data[5]), 342 cpucores=str(data[6]), 343 cputhreads=str(data[7]) 344 ) 345 return info 346 347 def list_vms(self, state=None): 348 self.conn = self.__get_conn() 349 vms = self.conn.find_vm(-1) 350 results = [] 351 for x in vms: 352 try: 353 if state: 354 vmstate = self.conn.get_status2(x) 355 if vmstate == state: 356 results.append(x.name()) 357 else: 358 results.append(x.name()) 359 except Exception: 360 pass 361 return results 362 363 def virttype(self): 364 return self.__get_conn().get_type() 365 366 def autostart(self, vmid, as_flag): 367 self.conn = self.__get_conn() 368 # Change autostart flag only if needed 369 if self.conn.get_autostart(vmid) != as_flag: 370 self.conn.set_autostart(vmid, as_flag) 371 return True 372 373 return False 374 375 def freemem(self): 376 self.conn = self.__get_conn() 377 return self.conn.getFreeMemory() 378 379 def shutdown(self, vmid): 380 """ Make the machine with the given vmid stop running. Whatever that takes. """ 381 self.__get_conn() 382 self.conn.shutdown(vmid) 383 return 0 384 385 def pause(self, vmid): 386 """ Pause the machine with the given vmid. """ 387 388 self.__get_conn() 389 return self.conn.suspend(vmid) 390 391 def unpause(self, vmid): 392 """ Unpause the machine with the given vmid. """ 393 394 self.__get_conn() 395 return self.conn.resume(vmid) 396 397 def create(self, vmid): 398 """ Start the machine via the given vmid """ 399 400 self.__get_conn() 401 return self.conn.create(vmid) 402 403 def start(self, vmid): 404 """ Start the machine via the given id/name """ 405 406 self.__get_conn() 407 return self.conn.create(vmid) 408 409 def destroy(self, vmid): 410 """ Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """ 411 self.__get_conn() 412 return self.conn.destroy(vmid) 413 414 def undefine(self, vmid): 415 """ Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """ 416 417 self.__get_conn() 418 return self.conn.undefine(vmid) 419 420 def status(self, vmid): 421 """ 422 Return a state suitable for server consumption. Aka, codes.py values, not XM output. 423 """ 424 self.__get_conn() 425 return self.conn.get_status(vmid) 426 427 def get_xml(self, vmid): 428 """ 429 Receive a Vm id as input 430 Return an xml describing vm config returned by a libvirt call 431 """ 432 433 self.__get_conn() 434 return self.conn.get_xml(vmid) 435 436 def get_maxVcpus(self, vmid): 437 """ 438 Gets the max number of VCPUs on a guest 439 """ 440 441 self.__get_conn() 442 return self.conn.get_maxVcpus(vmid) 443 444 def get_max_memory(self, vmid): 445 """ 446 Gets the max memory on a guest 447 """ 448 449 self.__get_conn() 450 return self.conn.get_MaxMemory(vmid) 451 452 def define(self, xml): 453 """ 454 Define a guest with the given xml 455 """ 456 self.__get_conn() 457 return self.conn.define_from_xml(xml) 458 459 460def core(module): 461 462 state = module.params.get('state', None) 463 autostart = module.params.get('autostart', None) 464 guest = module.params.get('name', None) 465 command = module.params.get('command', None) 466 uri = module.params.get('uri', None) 467 xml = module.params.get('xml', None) 468 469 v = Virt(uri, module) 470 res = dict() 471 472 if state and command == 'list_vms': 473 res = v.list_vms(state=state) 474 if not isinstance(res, dict): 475 res = {command: res} 476 return VIRT_SUCCESS, res 477 478 if autostart is not None and command != 'define': 479 if not guest: 480 module.fail_json(msg="autostart requires 1 argument: name") 481 try: 482 v.get_vm(guest) 483 except VMNotFound: 484 module.fail_json(msg="domain %s not found" % guest) 485 res['changed'] = v.autostart(guest, autostart) 486 if not command and not state: 487 return VIRT_SUCCESS, res 488 489 if state: 490 if not guest: 491 module.fail_json(msg="state change requires a guest specified") 492 493 if state == 'running': 494 if v.status(guest) == 'paused': 495 res['changed'] = True 496 res['msg'] = v.unpause(guest) 497 elif v.status(guest) != 'running': 498 res['changed'] = True 499 res['msg'] = v.start(guest) 500 elif state == 'shutdown': 501 if v.status(guest) != 'shutdown': 502 res['changed'] = True 503 res['msg'] = v.shutdown(guest) 504 elif state == 'destroyed': 505 if v.status(guest) != 'shutdown': 506 res['changed'] = True 507 res['msg'] = v.destroy(guest) 508 elif state == 'paused': 509 if v.status(guest) == 'running': 510 res['changed'] = True 511 res['msg'] = v.pause(guest) 512 else: 513 module.fail_json(msg="unexpected state") 514 515 return VIRT_SUCCESS, res 516 517 if command: 518 if command in VM_COMMANDS: 519 if command == 'define': 520 if not xml: 521 module.fail_json(msg="define requires xml argument") 522 if guest: 523 # there might be a mismatch between quest 'name' in the module and in the xml 524 module.warn("'xml' is given - ignoring 'name'") 525 try: 526 domain_name = re.search('<name>(.*)</name>', xml).groups()[0] 527 except AttributeError: 528 module.fail_json(msg="Could not find domain 'name' in xml") 529 530 # From libvirt docs (https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDefineXML): 531 # -- A previous definition for this domain would be overridden if it already exists. 532 # 533 # In real world testing with libvirt versions 1.2.17-13, 2.0.0-10 and 3.9.0-14 534 # on qemu and lxc domains results in: 535 # operation failed: domain '<name>' already exists with <uuid> 536 # 537 # In case a domain would be indeed overwritten, we should protect idempotency: 538 try: 539 existing_domain_xml = v.get_vm(domain_name).XMLDesc( 540 libvirt.VIR_DOMAIN_XML_INACTIVE 541 ) 542 except VMNotFound: 543 existing_domain_xml = None 544 try: 545 domain = v.define(xml) 546 if existing_domain_xml: 547 # if we are here, then libvirt redefined existing domain as the doc promised 548 if existing_domain_xml != domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE): 549 res = {'changed': True, 'change_reason': 'config changed'} 550 else: 551 res = {'changed': True, 'created': domain.name()} 552 except libvirtError as e: 553 if e.get_error_code() != 9: # 9 means 'domain already exists' error 554 module.fail_json(msg='libvirtError: %s' % e.get_error_message()) 555 if autostart is not None and v.autostart(domain_name, autostart): 556 res = {'changed': True, 'change_reason': 'autostart'} 557 558 elif not guest: 559 module.fail_json(msg="%s requires 1 argument: guest" % command) 560 else: 561 res = getattr(v, command)(guest) 562 if not isinstance(res, dict): 563 res = {command: res} 564 565 return VIRT_SUCCESS, res 566 567 elif hasattr(v, command): 568 res = getattr(v, command)() 569 if not isinstance(res, dict): 570 res = {command: res} 571 return VIRT_SUCCESS, res 572 573 else: 574 module.fail_json(msg="Command %s not recognized" % command) 575 576 module.fail_json(msg="expected state or command parameter to be specified") 577 578 579def main(): 580 module = AnsibleModule( 581 argument_spec=dict( 582 name=dict(type='str', aliases=['guest']), 583 state=dict(type='str', choices=['destroyed', 'paused', 'running', 'shutdown']), 584 autostart=dict(type='bool'), 585 command=dict(type='str', choices=ALL_COMMANDS), 586 uri=dict(type='str', default='qemu:///system'), 587 xml=dict(type='str'), 588 ), 589 ) 590 591 if not HAS_VIRT: 592 module.fail_json(msg='The `libvirt` module is not importable. Check the requirements.') 593 594 rc = VIRT_SUCCESS 595 try: 596 rc, result = core(module) 597 except Exception as e: 598 module.fail_json(msg=to_native(e), exception=traceback.format_exc()) 599 600 if rc != 0: # something went wrong emit the msg 601 module.fail_json(rc=rc, msg=result) 602 else: 603 module.exit_json(**result) 604 605 606if __name__ == '__main__': 607 main() 608