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