diff --git a/.gitignore b/.gitignore index acbc4c8..18d65d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,11 @@ # directory generated by python installer build/* +dist +MANIFEST # doc build directory doc/build/* # virtualenv -env/ -env3/ +env*/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..559e591 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include kvm/kvm.json +include README.rst diff --git a/README.md b/README.md deleted file mode 100644 index f2ddecc..0000000 --- a/README.md +++ /dev/null @@ -1,48 +0,0 @@ -python-kvm -========== - -This module aims to manage KVM hypervisors. For this it use the -`unix module `_ which allow to manage -Unix-like systems, both locally and remotely, in the same by overloading class -instances. This module is just a wrapper to the ``virsh`` command. It parse -outputs of the ``virsh`` command (both XML and text). Commands are grouped in -childs objects accessible via properties. - -Installation ------------- -This module is compatible with python2.7 and python 3.*. The module is -on **PyPi** so you can use the ``pip`` command for installing it. - -For example, to use ``kvm`` in a virtualenv: - -.. code:: bash - - $ virtualenv env/ --prompt "(myprog)" - $ . ./env/bin/activate - (myprog) $ pip install kvm - -Otherwise sources are on github: https://github.com/fmenabe/python-kvm - -Usage ------ -You need to import the necessary classes from ``unix`` module. An hypervisor is -represented by the **Hypervisor** class and must wrap an object of type -``unix.Local`` or ``unix.Remote``. It theorically support any Unix system, but -disks manipulations need *nbd* module to be loaded so it is better to use an -``unix.linux.Linux`` host. - -.. code-block:: python - - >>> from unix import Local, Remote, UnixError - >>> from unix.linux import Linux - >>> import kvm - >>> localhost = kvm.Hypervisor(Linux(Local())) - >>> localhost.generic.nodeinfo() - {'nb_cpu': 1, - 'nb_threads_per_core': 2, - 'memory': 16331936, - 'numa_cells': 1, - 'cpu_model': 'x86_64', - 'nb_cores_per_cpu': 4, - 'nb_cores': 8, - 'cpu_freq': 1340} diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..df30475 --- /dev/null +++ b/README.rst @@ -0,0 +1,99 @@ +python-kvm +========== + +This module aims to manage KVM hypervisors. For this it use the +`unix module `_ which allow to manage +Unix-like systems, both locally and remotely, in the same by overloading class +instances. This module is just a wrapper to the ``virsh`` command. It parse +outputs of the ``virsh`` command (both XML and text). Commands are grouped in +childs objects accessible via properties. + +Installation +------------ +This module is compatible with python2.7 and python 3.*. The module is +on **PyPi** so you can use the ``pip`` command for installing it. + +For example, to use ``kvm`` in a virtualenv: + +.. code:: bash + + $ virtualenv env/ --prompt "(myprog)" + $ . ./env/bin/activate + (myprog) $ pip install kvm + +Otherwise sources are on github: https://github.com/fmenabe/python-kvm + +Usage +----- +You need to import the necessary classes from ``unix`` module. An hypervisor is +represented by the **Hypervisor** object and must wrap an object of type +``unix.Local`` or ``unix.Remote``. It theorically support any Unix system, but +disks manipulations need *nbd* module to be loaded so it is better to use an +``unix.linux.Linux`` host. + +.. code-block:: python + + >>> from unix import Local, Remote, UnixError + >>> from unix.linux import Linux + >>> import kvm + >>> import json + >>> localhost = kvm.Hypervisor(Linux(Local())) + >>> localhost.hypervisor.nodeinfo() + {'nb_cpu': 1, + 'nb_threads_per_core': 2, + 'memory': 16331936, + 'numa_cells': 1, + 'cpu_model': 'x86_64', + 'nb_cores_per_cpu': 4, + 'nb_cores': 8, + 'cpu_freq': 1340} + >>> localhost.list_domains(all=True) + {'guest1': {'id': -1, 'state': 'shut off'}} + {'guest2': {'id': 1, 'state': 'running'}} + >>> localhost.domain.start('guest1') + # Wait a few seconds for the domain to start. + >>> localhost.domain.state('guest1') + 'running' + >>> localhost.domain.id('guest1') + 2 + >>> print(json.dumps(localhost.domain.conf('guest1'), indent=2)) + # json is use for pretty printing the dictionnary containing the + # configuration. + { + "@type": "kvm", + "name": "guest1", + "uuid": "ed68d942-5d4b-7bba-4d74-7d44d73779d3", + "memory": { + "@unit": "KiB", + "#text": "2097152" + }, + ... + } + >>> localhost.list_networks() + {'default': {'autostart': True, 'persistent': True, 'state': 'active'}} + + >>> host = Remote() + >>> host.connect('hypervisor1') + >>> host = kvm.Hypervisor(Linux(host)) + >>> host.hypervisor.nodeinfo() + {'cores_per_socket': 12, + 'cpu_frequency': '2200 MHz', + 'cpu_model': 'x86_64', + 'cpu_sockets': 2, + 'cpus': 24, + 'memory_size': '98974432 kB', + 'numa_cells': 1, + 'threads_per_core': 1} + >>> host.list_domains(all=True) + {'guest1': {'id': 1, 'state': 'running'}} + {'guest2': {'id': 2, 'state': 'running'}} + >>> host.domain.shutdown('guest2') + # Wait for the domain to stop. + >>> host.domain.state('guest1') + 'shut off' + + # Using the context manager for the connecion. + >>> from unix.linux as linux, kvm + >>> with linux.connect('hypervisor1') as host: + ... host = kvm.Hypervisor(host) + ... host.hypervisor.node_info() diff --git a/doc/source/examples.rst b/doc/source/examples.rst new file mode 100644 index 0000000..6003a4f --- /dev/null +++ b/doc/source/examples.rst @@ -0,0 +1,974 @@ +******** +Examples +******** + +.. code:: + + >>> import unix, kvm + >>> host = unix.Remote() + >>> host.connect('remote_host') + >>> host = kvm.Hypervisor(host) + +This is for testing purpose. In general, it is probably better to use +the ``unix.connect`` context manager (which close the connection at when +quitting): + +.. code:: + + with unix.connect('remote_host') as host: + host = kvm.Hypervisor(host) + +Managing the hypervisor +======================= +Virsh version +~~~~~~~~~~~~~ +.. code:: + + >>> host.hypervisor.version() + {'compiled_against_library': 'libvirt 1.2.2', + 'running_hypervisor': 'QEMU 2.0.0', + 'using_api': 'QEMU 1.2.2', + 'using_library': 'libvirt 1.2.2'} + +URI +~~~ +.. code:: + + >>> host.hypervisor.uri() + 'qemu:///system' + +System information +~~~~~~~~~~~~~~~~~~ +.. code:: + + >>> host.hypervisor.sysinfo() + {'bios': {'date': '02/06/2014', 'vendor': 'HP', 'version': 'A28'}, + 'memory_device': [{'bank_locator': 'Not Specified', + 'form_factor': 'DIMM', + 'locator': 'Proc 1 DIMM 1A', + 'manufacturer': 'HP', + 'part_number': '647650-171', + 'serial_number': 'Not Specified', + 'size': '8192 MB', + 'speed': '1333 MHz', + 'type': 'DDR3', + 'type_detail': 'Synchronous Registered (Buffered)'}, + {'bank_locator': 'Not Specified', + 'form_factor': 'DIMM', + 'locator': 'Proc 1 DIMM 3E', + 'manufacturer': 'HP', + 'part_number': '647650-171', + 'serial_number': 'Not Specified', + 'size': '8192 MB', + 'speed': '1333 MHz', + 'type': 'DDR3', + 'type_detail': 'Synchronous Registered (Buffered)'}, + ... + {'bank_locator': 'Not Specified', + 'form_factor': 'DIMM', + 'locator': 'Proc 2 DIMM 12H', + 'manufacturer': 'HP', + 'part_number': '647650-171', + 'serial_number': 'Not Specified', + 'size': '8192 MB', + 'speed': '1333 MHz', + 'type': 'DDR3', + 'type_detail': 'Synchronous Registered (Buffered)'}], + 'processor': [{'external_clock': '200 MHz', + 'family': 'Opteron', + 'manufacturer': 'AMD', + 'max_speed': '3500 MHz', + 'part_number': 'Not Specified', + 'serial_number': 'Not Specified', + 'signature': 'Family 21, Model 2, Stepping 0', + 'socket_destination': 'Proc 1', + 'status': 'Populated, Enabled', + 'type': 'Central Processor', + 'version': 'AMD Opteron(tm) Processor 6376'}, + {'external_clock': '200 MHz', + 'family': 'Opteron', + 'manufacturer': 'AMD', + 'max_speed': '3500 MHz', + 'part_number': 'Not Specified', + 'serial_number': 'Not Specified', + 'signature': 'Family 21, Model 2, Stepping 0', + 'socket_destination': 'Proc 2', + 'status': 'Populated, Idle', + 'type': 'Central Processor', + 'version': 'AMD Opteron(tm) Processor 6376'}], + 'system': {'family': 'ProLiant', + 'manufacturer': 'HP', + 'product': 'ProLiant DL385p Gen8', + 'serial': 'CZJ4020390', + 'sku': '703932-421', + 'uuid': '39333037-3233-5A43-4A34-303230333930', + 'version': 'Not Specified'}, + 'type': 'smbios'} + +Basic information about the node +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: + + >>> host.hypervisor.nodeinfo() + {'cores_per_socket': 32, + 'cpu_frequency': '1400 MHz', + 'cpu_model': 'x86_64', + 'cpu_sockets': 1, + 'cpus': 32, + 'memory_size': '131919564 KiB', + 'numa_cells': 1, + 'threads_per_core': 1} + +CPU map +~~~~~~~ +.. code:: + + >>> host.hypervisor.nodecpumap() + {'cpu_map': 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy', + 'cpus_online': 32, + 'cpus_present': 32} + +CPU stats +~~~~~~~~~ +.. code:: + + >>> host.hypervisor.nodecpustats() + {'idle': 67050204750000000, + 'iowait': 47793370000000, + 'system': 1004314090000000, + 'user': 2927654340000000} + + >>> host.hypervisor.nodecpustats(percent=True) + {'idle': '90.3%', + 'iowait': '0.1%', + 'system': '1.8%', + 'usage': '9.6%', + 'user': '7.8%'} + + >>> host.hypervisor.nodecpustats(31, percent=True) + {'idle': '97.0%', + 'iowait': '0.0%', + 'system': '1.0%', + 'usage': '3.0%', + 'user': '2.0%'} + +Memory stats +~~~~~~~~~~~~ +.. code:: + + >>> host.hypervisor.nodememstats() + {'buffers': '246688 KiB', + 'cached': '97146740 KiB', + 'free': '2155148 KiB', + 'total': '131919564 KiB'} + + >>> host.hypervisor.nodememstats(0) + {'free': '1138132 KiB', 'total': '32848952 KiB'} + +Tune memory parameters +~~~~~~~~~~~~~~~~~~~~~~ +.. code:: + + >>> host.hypervisor.node_memory_tune() + {'shm_full_scans': 138, + 'shm_merge_across_nodes': 1, + 'shm_pages_shared': 424645, + 'shm_pages_sharing': 3721845, + 'shm_pages_to_scan': 100, + 'shm_pages_unshared': 3907333, + 'shm_pages_volatile': 2108845, + 'shm_sleep_millisecs': 200} + + >>> host.hypervisor.node_memory_tune(shm_pages_to_scan=150, shm_sleep_millisecs=100) + (True, '', '') + + >>> host.hypervisor.node_memory_tune() + {'shm_full_scans': 138, + 'shm_merge_across_nodes': 1, + 'shm_pages_shared': 424622, + 'shm_pages_sharing': 3721888, + 'shm_pages_to_scan': 150, + 'shm_pages_unshared': 3910168, + 'shm_pages_volatile': 2105990, + 'shm_sleep_millisecs': 100} + +Suspend host +~~~~~~~~~~~~ +.. code:: + + >>> host.hypervisor.nodesuspend('mem', 60) + (True, '', '') + +Capabilities +~~~~~~~~~~~~ +.. code:: + + >>> kvm.pprint(host.hypervisor.capabilities()) + {'guest': [{'arch': {'@name': 'i686', + 'domain': [{'@type': 'qemu'}, + {'@type': 'kvm', + 'emulator': '/usr/bin/kvm-spice', + 'machine': [{'#text': 'pc', + '@canonical': 'pc-i440fx-trusty', + '@maxCpus': '255'}, + {'#text': 'pc-1.3', '@maxCpus': '255'}, + ... + {'#text': 'pc-0.13', '@maxCpus': '255'}]}], + 'emulator': '/usr/bin/qemu-system-i386', + 'machine': [{'#text': 'pc', + '@canonical': 'pc-i440fx-trusty', + '@maxCpus': '255'}, + {'#text': 'pc-0.12', '@maxCpus': '255'}, + ... + {'#text': 'pc-0.13', '@maxCpus': '255'}], + 'wordsize': '32'}, + 'features': {'acpi': {'@default': 'on', '@toggle': 'yes'}, + 'apic': {'@default': 'on', '@toggle': 'no'}, + 'cpuselection': True, + 'deviceboot': True, + 'nonpae': True, + 'pae': True}, + 'os_type': 'hvm'}, + {'arch': {'@name': 'x86_64', + 'domain': [{'@type': 'qemu'}, + {'@type': 'kvm', + 'emulator': '/usr/bin/kvm-spice', + 'machine': [{'#text': 'pc', + '@canonical': 'pc-i440fx-trusty', + '@maxCpus': '255'}, + {'#text': 'pc-1.3', '@maxCpus': '255'}, + ... + {'#text': 'pc-0.13', '@maxCpus': '255'}]}], + 'emulator': '/usr/bin/qemu-system-x86_64', + 'machine': [{'#text': 'pc', + '@canonical': 'pc-i440fx-trusty', + '@maxCpus': '255'}, + {'#text': 'pc-1.3', '@maxCpus': '255'}, + ... + {'#text': 'pc-0.13', '@maxCpus': '255'}], + 'wordsize': '64'}, + 'features': {'acpi': {'@default': 'on', '@toggle': 'yes'}, + 'apic': {'@default': 'on', '@toggle': 'no'}, + 'cpuselection': True, + 'deviceboot': True}, + 'os_type': 'hvm'}], + 'host': {'cpu': {'arch': 'x86_64', + 'feature': [{'@name': 'bmi1'}, + {'@name': 'perfctr_nb'}, + {'@name': 'perfctr_core'}, + {'@name': 'topoext'}, + {'@name': 'nodeid_msr'}, + {'@name': 'tce'}, + {'@name': 'lwp'}, + {'@name': 'wdt'}, + {'@name': 'skinit'}, + {'@name': 'ibs'}, + {'@name': 'osvw'}, + {'@name': 'cr8legacy'}, + {'@name': 'extapic'}, + {'@name': 'cmp_legacy'}, + {'@name': 'fxsr_opt'}, + {'@name': 'mmxext'}, + {'@name': 'osxsave'}, + {'@name': 'monitor'}, + {'@name': 'ht'}, + {'@name': 'vme'}], + 'model': 'Opteron_G5', + 'topology': {'@cores': '32', '@sockets': '1', '@threads': '1'}, + 'vendor': 'AMD'}, + 'migration_features': {'live': True, + 'uri_transports': {'uri_transport': 'tcp'}}, + 'power_management': {'suspend_disk': True, 'suspend_hybrid': True}, + 'secmodel': [{'doi': '0', 'model': 'apparmor'}, + {'baselabel': [{'#text': '+110:+117', '@type': 'kvm'}, + {'#text': '+110:+117', '@type': 'qemu'}], + 'doi': '0', + 'model': 'dac'}], + 'topology': {'cells': {'@num': '4', + 'cell': [{'@id': '0', + 'cpus': {'@num': '8', + 'cpu': [{'@core_id': '0', + '@id': '0', + '@siblings': '0,2', + '@socket_id': '0'}, + {'@core_id': '1', '@id': '2', '@siblings': '0,2', '@socket_id': '0'}, + {'@core_id': '2', '@id': '4', '@siblings': '4,6', '@socket_id': '0'}, + {'@core_id': '3', '@id': '6', '@siblings': '4,6', '@socket_id': '0'}, + {'@core_id': '4', '@id': '8', '@siblings': '8,10', '@socket_id': '0'}, + {'@core_id': '5', '@id': '10', '@siblings': '8,10', '@socket_id': '0'}, + {'@core_id': '6', + '@id': '12', + '@siblings': '12,14', + '@socket_id': '0'}, + {'@core_id': '7', + '@id': '14', + '@siblings': '12,14', + '@socket_id': '0'}]}, + 'memory': {'#text': '32848952', '@unit': 'KiB'}}, + {'@id': '1', + 'cpus': {'@num': '8', + 'cpu': [{'@core_id': '0', + '@id': '16', + '@siblings': '16,18', + '@socket_id': '0'}, + .... + {'@core_id': '7', + '@id': '30', + '@siblings': '28,30', + '@socket_id': '0'}]}, + 'memory': {'#text': '33029144', '@unit': 'KiB'}}, + {'@id': '2', + 'cpus': {'@num': '8', + 'cpu': [{'@core_id': '0', + '@id': '1', + '@siblings': '1,3', + '@socket_id': '1'}, + ... + {'@core_id': '7', + '@id': '15', + '@siblings': '13,15', + '@socket_id': '1'}]}, + 'memory': {'#text': '33029148', '@unit': 'KiB'}}, + {'@id': '3', + 'cpus': {'@num': '8', + 'cpu': [{'@core_id': '0', + '@id': '17', + '@siblings': '17,19', + '@socket_id': '1'}, + ... + {'@core_id': '7', + '@id': '31', + '@siblings': '29,31', + '@socket_id': '1'}]}, + 'memory': {'#text': '33012320', '@unit': 'KiB'}}]}}, + 'uuid': '39333037-3233-5a43-4a34-303230333930'}} + +.. note:: By default the method that parse XML files return an **OrderedDict** for keeping order. ``kvm.pprint()`` function allow to pretty print **OrderedDict** dicts. + +Freecell +~~~~~~~~ +.. code:: + + >>> host.hypervisor.freecell(all=True) + {'0': '1034804 KiB', + '1': '501332 KiB', + '2': '268616 KiB', + '3': '322696 KiB', + 'total': '2127448 KiB'} + + >>> host.hypervisor.freecell(cellno=0) + {'0': '1020744 KiB'} + +Managing interfaces +=================== +List +~~~~ +.. code:: + + >>> host.list_interfaces() + {'br0': {'mac': '64:70:02:00:6a:95', 'state': 'active'}, + 'lo': {'mac': '00:00:00:00:00:00', 'state': 'active'}} + +Conf +~~~~ +.. code:: + + >>> kvm.pprint(host.iface.conf('br0')) + {'@name': 'br0', + '@type': 'bridge', + 'bridge': {'interface': {'@name': 'enp3s5', + '@type': 'ethernet', + 'link': {'@speed': '1000', '@state': 'up'}, + 'mac': {'@address': '64:70:02:00:6a:95'}}}, + 'protocol': [{'@family': 'ipv4', + 'ip': {'@address': '192.168.0.10', '@prefix': '24'}}, + {'@family': 'ipv6', + 'ip': {'@address': 'fe80::6670:2ff:fe00:6a95', '@prefix': '64'}}]} + +Managing networks +================= +List +~~~~ +.. code:: + + >>> host.list_networks(all=True) + {'default': {'autostart': True, 'persistent': True, 'state': 'active'}} + +Conf +~~~~ +.. code:: + + >>> kvm.pprint(host.net.conf('default')) + {'bridge': {'@delay': '0', '@name': 'virbr0', '@stp': 'on'}, + 'forward': {'@mode': 'nat', + 'nat': {'port': {'@end': '65535', '@start': '1024'}}}, + 'ip': {'@address': '192.168.122.1', + '@netmask': '255.255.255.0', + 'dhcp': {'range': {'@end': '192.168.122.254', '@start': '192.168.122.2'}}}, + 'mac': {'@address': '52:54:00:d1:7f:f7'}, + 'name': 'default', + 'uuid': '403015c8-8339-4a66-bc37-ec794bc39e9d'} + +Destroy (stop) +~~~~~~~~~~~~~~ +.. code:: + + >>> host.net.destroy('default') + (True, 'Network default destroyed', '') + + >>> host.list_networks(all=True) + {'default': {'autostart': True, 'persistent': True, 'state': 'inactive'}} + +Undefine +~~~~~~~~ +.. code:: + + >>> host.net.undefine('default') + (True, 'Network default has been undefined', '') + + >>> host.list_networks(all=True) + {} + +Create +~~~~~~ +.. code:: + + >>> net = {'name': 'br0', 'forward': {'@mode': 'bridge'}, 'bridge': {'@name': 'br0'}} + >>> with host.open('/vm/conf/networks/br0.xml', 'w') as fhandler: + ... fhandler.write(kvm.to_xml('network', net)) + + >>> host.network.define('/vm/conf/networks/br0.xml') + (True, 'Network br0 defined from /vm/conf/networks/br0.xml', '') + + >>> host.network.autostart('br0') + (True, 'Network br0 marked as autostarted', '') + + >>> host.network.start('br0') + (True, 'Network br0 started', '') + + >>> host.list_networks() + {'br0': {'autostart': True, 'persistent': True, 'state': 'active'}} + +Managing storage pools +====================== +List +~~~~ +.. code:: + + >>> host.list_pools(all=True) + {} + +Define +~~~~~~ +.. code:: + + >>> pool = {'@type': 'dir', + 'name': 'default', + 'source': True, + 'target': {'path': '/vm/disk', + 'permissions': {'group': '-1', + 'mode': '0711', + 'owner': '-1'}}} + >>> with host.open('/tmp/pool.xml', 'w') as fhandler: + ... fhandler.write(kvm.to_xml('pool', pool)) + >>> host.pool.define('/tmp/pool.xml') + (True, 'Pool default defined from /tmp/pool.xml', '') + + >>> host.list_pools(all=True, details=True) + {'default': {'allocation': '-', + 'autostart': False, + 'available': '', + 'capacity': '- -', + 'persistent': True, + 'state': 'inactive'}} + +Build +~~~~~ +.. code:: + + >>> host.listdir('/vm') + [] + + >>> host.pool.build('default') + (True, 'Pool default built', '') + + >>> host.listdir('/vm') + ['disk'] + +Start +~~~~~ +.. code:: + + >>> host.pool.start('default') + (True, 'Pool default started', '') + + >>> host.list_pools(all=True, details=True) + {'default': {'allocation': '2.48 GiB', + 'autostart': False, + 'available': '11.14 GiB', + 'capacity': '13.62 GiB', + 'persistent': True, + 'state': 'running'}} + +Autostart +~~~~~~~~~ +.. code:: + + >>> host.pool.autostart('default') + (True, 'Pool default marked as autostarted', '') + + >>> host.list_pools(all=True) + {'default': {'autostart': True, 'state': 'active'}} + + >>> host.pool.autostart('default', disable=True) + (True, 'Pool default unmarked as autostarted', '') + + >>> host.list_pools(all=True) + {'default': {'autostart': False, 'state': 'active'}} + +Info +~~~~ +.. code:: + + >>> host.pool.info('default') + {'allocation': '2.48 GiB', + 'autostart': False, + 'available': '11.14 GiB', + 'capacity': '13.62 GiB', + 'name': 'default', + 'persistent': True, + 'state': 'running', + 'uuid': '28d614d5-7e17-40fc-b866-cc4bd26eab47'} + +Conf +~~~~ +.. code:: + + >>> kvm.pprint(host.pool.conf('default')) + {'@type': 'dir', + 'allocation': {'#text': '2663366656', '@unit': 'bytes'}, + 'available': {'#text': '11965825024', '@unit': 'bytes'}, + 'capacity': {'#text': '14629191680', '@unit': 'bytes'}, + 'name': 'default', + 'source': True, + 'target': {'path': '/vm/disk', + 'permissions': {'group': '0', 'mode': '0711', 'owner': '0'}}, + 'uuid': '28d614d5-7e17-40fc-b866-cc4bd26eab47'} + +Uuid +~~~~ +.. code:: + + >>> host.pool.uuid('default') + '28d614d5-7e17-40fc-b866-cc4bd26eab47' + +Name +~~~~ +.. code:: + + >>> host.pool.name('28d614d5-7e17-40fc-b866-cc4bd26eab47') + 'default' + +Destroy +~~~~~~~ +.. code:: + + >>> host.pool.destroy('default') + (True, 'Pool default destroyed', '') + + >>> host.list_pools(all=True) + {'default': {'autostart': False, 'state': 'inactive'}} + +Undefine +~~~~~~~~ +.. code:: + + >>> host.pool.undefine('default') + (True, 'Pool default has been undefined', '') + + >>> host.list_pools(all=True) + {} + +Create +~~~~~~ +.. code:: + + >>> host.pool.create('/tmp/pool.xml') + (True, 'Pool default created from /tmp/pool.xml', '') + + >>> host.list_pools(all=True, details=True) + {'default': {'allocation': '2.48 GiB', + 'autostart': False, + 'available': '11.14 GiB', + 'capacity': '13.62 GiB', + 'persistent': False, + 'state': 'running'}} + +Delete +~~~~~~ + + +Managing volumes +================ +Create +~~~~~~ +.. code:: + + >>> host.volume.create_as('default', 'disk.qcow2', '20G', format='qcow2') + (True, 'Vol disk.qcow2 created', '') + + >>> host.list_volumes('default', details=True) + {'disk.qcow2': {'allocation': '196.00 KiB', + 'capacity': '20.00 GiB', + 'path': '/vm/disk/disk.qcow2', + 'type': 'file'}} + +.. code:: + + >>> vol = {'@type': 'file', + 'capacity': {'#text': '5368709120', '@unit': 'bytes'}, + 'key': '/vm/disk/disk3.qcow2', + 'name': 'disk3.qcow2', + 'source': True, + 'target': {'format': {'@type': 'qcow2'}, 'path': '/vm/disk/disk3.qcow2'}} + + >>> with host.open('/tmp/volume.xml', 'w') as fhandler: + fhandler.write(kvm.to_xml('volume', vol)) + + >>> host.volume.create('default', '/tmp/volume.xml') + (True, 'Vol disk3.qcow2 created from /tmp/volume.xml', '') + +Delete +~~~~~~ +.. code:: + + >>> host.volume.delete('disk.qcow2', pool='default') + (True, 'Vol disk.qcow2 deleted', '') + +Info +~~~~ +.. code:: + + >>> host.volume.info('disk.qcow2', pool='default') + {'allocation': '3.32 MiB', + 'capacity': '20.00 GiB', + 'name': 'disk.qcow2', + 'type': 'file'} + +Conf +~~~~ +.. code:: + + >>> kvm.pprint(host.volume.conf('disk.qcow2', pool='default')) + {'@type': 'file', + 'allocation': {'#text': '3485696', '@unit': 'bytes'}, + 'capacity': {'#text': '21474836480', '@unit': 'bytes'}, + 'key': '/vm/disk/disk.qcow2', + 'name': 'disk.qcow2', + 'source': True, + 'target': {'format': {'@type': 'qcow2'}, + 'path': '/vm/disk/disk.qcow2', + 'permissions': {'group': '0', 'mode': '0600', 'owner': '0'}, + 'timestamps': {'atime': '1453761773.202566393', + 'ctime': '1453761770.938733302', + 'mtime': '1453761770.918734776'}}} + +Wipe +~~~~ +.. code:: + + >>> host.volume.wipe('disk.qcow2', pool='default', algorithm='dod') + +.. note:: Other algorithms than *zero* need the ``scrub`` package to be installed. + +Clone +~~~~~ +.. code:: + + >>> host.volume.clone('disk.qcow2', 'disk2.qcow2', pool='default', prealloc_metadata=True) + (True, 'Vol disk2.qcow2 cloned from disk.qcow2', '') + + >>> host.list_volumes('default', details=True) + {'disk.qcow2': {'allocation': '524.00 KiB', + 'capacity': '2.00 GiB', + 'path': '/vm/disk/disk.qcow2', + 'type': 'file'}, + 'disk2.qcow2': {'allocation': '524.00 KiB', + 'capacity': '2.00 GiB', + 'path': '/vm/disk/disk2.qcow2', + 'type': 'file'}} + +Resize +~~~~~~ +.. code:: + + >>> host.volume.resize('disk.qcow2', '5GiB', pool='default') + (True, "Size of volume 'disk.qcow2' successfully changed to 5GiB", '') + + >>> host.list_volumes('default', details=True) + {'disk.qcow2': {'allocation': '528.00 KiB', + 'capacity': '5.00 GiB', + 'path': '/vm/disk/disk.qcow2', + 'type': 'file'}, + 'disk2.qcow2': {'allocation': '524.00 KiB', + 'capacity': '2.00 GiB', + 'path': '/vm/disk/disk2.qcow2', + 'type': 'file'}} + +Upload/Download +~~~~~~~~~~~~~~~ +.. code:: + + >>> host.listdir('/vm') + ['disk', 'modele-trusty.qcow2'] + + >>> host.volume.create_as('default', 'trusty.qcow2', '1GiB') + (True, 'Vol trusty.qcow2 created', '') + + >>> host.volume.upload(pool='default', file='/vm/modele-trusty.qcow2', vol='trusty.qcow2') + (True, '', '') + + >>> host.list_volumes('default', details=True) + {'trusty.qcow2': {'allocation': '1.83 GiB', + 'capacity': '30.00 GiB', + 'path': '/vm/disk/trusty.qcow2', + 'type': 'file'}} + + >>> host..volume.download(pool='default', file='/vm/new.qcow2', vol='trusty.qcow2')Out[205]: (True, '', '') + + >>> host.listdir('/vm') + ['disk', 'modele-trusty.qcow2', 'new.qcow2'] + +Secrets +======= +Define +~~~~~~ +.. code:: + + >>> secret = {'@ephemeral': 'no', + ...: '@private': 'no', + ...: 'uuid': kvm.gen + ...: 'uuid': kvm.gen_uuid(), + ...: 'usage': {'@type': 'volume', + ...: 'volume': '/vm/disk/encrypted.qcow2'}} + + >>> with host.open('/tmp/secret.xml', 'w') as fhandler: + ...: fhandler.write(kvm.to_xml('secret', secret)) + ...: + + >>> host.secret.define('/tmp/secret.xml') + (True, 'Secret 6d14f73a-1087-7180-792d-8d80fc6b55ec created', '') + +List +~~~~ +.. code:: + + >>> host.list_secrets() + {'6d14f73a-1087-7180-792d-8d80fc6b55ec': 'volume /vm/disk/encrypted.qcow2'} + +Conf +~~~~ +.. code:: + + >>> kvm.pprint(host.secret.conf('6d14f73a-1087-7180-792d-8d80fc6b55ec')) + {'@ephemeral': 'no', + '@private': 'no', + 'usage': {'@type': 'volume', 'volume': '/vm/disk/encrypted.qcow2'}, + 'uuid': '6d14f73a-1087-7180-792d-8d80fc6b55ec'} + +Set value +~~~~~~~~~ +.. code:: + + >>> import base64 + >>> passphrase = base64.b64encode(b'passphrase').decode() + >>> host.secret.set_value('6d14f73a-1087-7180-792d-8d80fc6b55ec', passphrase) + (True, 'Secret value set', '') + +Get value +~~~~~~~~~ +.. code:: + + >>> base64.b64decode(host.secret.get_value('6d14f73a-1087-7180-792d-8d80fc6b55ec')).decode() + 'passphrase' + +Undefine +~~~~~~~~~ +.. code:: + + >>> host.secret.undefine('6d14f73a-1087-7180-792d-8d80fc6b55ec') + (True, 'Secret 6d14f73a-1087-7180-792d-8d80fc6b55ec deleted', '') + +Managing domains +================ +.. code:: + + >>> domain = {'@type': 'kvm', + 'name': 'trusty', + 'uuid': kvm.gen_uuid(), + 'title': 'Ubuntu 14.04', + 'memory': {'@unit': 'GiB', '#text': 2}, + 'currentMemory': {'@unit': 'GiB', '#text': 2}, + 'vcpu': {'#text': 2}, + 'os': {'type': {'@arch': 'x86_64', '@machine': 'pc', '#text': 'hvm'}, + 'boot': {'@dev': 'hd'}, + 'bootmenu': {'@enable': 'no'}}, + 'features': {'acpi': None, 'apic': None, 'pae': None}, + 'clock': {'@offset': 'utc'}, + 'on_poweroff': {'#text': 'destroy'}, + 'on_reboot': {'#text': 'restart'}, + 'on_crash': {'#text': 'restart'}, + 'devices': { + 'emulator': {'#text': '/usr/bin/kvm'}, + 'disk': [ + {'@type': 'volume', + '@device': 'disk', + 'driver': {'@name': 'qemu', '@type': 'qcow2'}, + 'source': {'@pool': 'default', '@volume': 'trusty.qcow2'}, + 'target': {'@dev': 'vda', '@bus': 'virtio'}} + ], + 'interface': [ + {'@type': 'network', + 'mac': {'@address': kvm.gen_mac()}, + 'model': {'@type': 'virtio'}, + 'source': {'@network': 'br0'}} + ], + 'serial': {'@type': 'pty', 'target': {'@port': 0}}, + 'console': {'@type': 'pty', 'target': {'@type': 'serial', '@port': 0}}, + 'input': [{'@type': 'mouse', '@bus': 'ps2'}, + {'@type': 'keyboard', '@bus': 'ps2'}], + 'graphics': {'@type': 'vnc', + '@port': -1, + '@autoport': 'yes', + '@keymap': 'fr', + 'listen': {'@type': 'address', '@address': '127.0.0.1'}}, + 'video': {'model': {'@type': 'cirrus'}}, + 'memballon': {'@type': 'virtio'} + } + } + + >>> with host.open('/vm/conf/trusty.xml', 'w') as fhandler: + ... fhandler.write(kvm.to_xml('domain', domain)) + + >>> host.domain.define('/vm/conf/trusty.xml') + (True, 'Domain trusty defined from /vm/conf/trusty.xml', '') + + >>> host.domain.start('trusty') + (True, 'Domain trusty started', '') + + >>> host.list_domains() + {'trusty': {'id': 2, 'state': 'running'}} + + +Snapshots +========= +.. code:: + + >>> host.snapshot.create_as('trusty') + (True, 'Domain snapshot 1453929671 created', '') + + >>> host.snapshot.info('trusty', '1453929671') + {'children': 0, + 'current': True, + 'descendants': 0, + 'domain': 'trusty', + 'location': 'internal', + 'metadata': True, + 'name': 1453929671, + 'parent': '-', + 'state': 'running'} + + >>> host.snapshot.create_as('trusty') + (True, 'Domain snapshot 1453929756 created', '') + + >>> host.snapshot.info('trusty', '1453929671') + {'children': 1, + 'current': False, + 'descendants': 1, + 'domain': 'trusty', + 'location': 'internal', + 'metadata': True, + 'name': 1453929671, + 'parent': '-', + 'state': 'running'} + + >>> host.snapshot.info('trusty', '1453929756') + {'children': 0, + 'current': True, + 'descendants': 0, + 'domain': 'trusty', + 'location': 'internal', + 'metadata': True, + 'name': 1453929756, + 'parent': 1453929671, + 'state': 'running'} + + >>> kvm.pprint(host.snapshot.conf('trusty', '1453929756')) + {'creationTime': '1453929756', + 'disks': {'disk': {'@name': 'vda', '@snapshot': 'internal'}}, + 'domain': {'@type': 'kvm', + 'clock': {'@offset': 'utc'}, + 'currentMemory': {'#text': '2097152', '@unit': 'KiB'}, + 'devices': {'console': {'@type': 'pty', + 'target': {'@port': '0', '@type': 'serial'}}, + 'controller': [{'@index': '0', '@type': 'usb'}, + {'@index': '0', '@model': 'pci-root', '@type': 'pci'}], + 'disk': {'@device': 'disk', + '@type': 'volume', + 'address': {'@bus': '0x00', + '@domain': '0x0000', + '@function': '0x0', + '@slot': '0x04', + '@type': 'pci'}, + 'driver': {'@name': 'qemu', '@type': 'qcow2'}, + 'source': {'@pool': 'default', '@volume': 'trusty.qcow2'}, + 'target': {'@bus': 'virtio', '@dev': 'vda'}}, + 'emulator': '/usr/bin/kvm', + 'graphics': {'@autoport': 'yes', + '@keymap': 'fr', + '@listen': '127.0.0.1', + '@port': '-1', + '@type': 'vnc', + 'listen': {'@address': '127.0.0.1', '@type': 'address'}}, + 'input': [{'@bus': 'ps2', '@type': 'mouse'}, + {'@bus': 'ps2', '@type': 'keyboard'}], + 'interface': {'@type': 'network', + 'address': {'@bus': '0x00', + '@domain': '0x0000', + '@function': '0x0', + '@slot': '0x03', + '@type': 'pci'}, + 'mac': {'@address': '54:52:00:cc:ba:4a'}, + 'model': {'@type': 'virtio'}, + 'source': {'@network': 'br0'}}, + 'memballoon': {'@model': 'virtio', + 'address': {'@bus': '0x00', + '@domain': '0x0000', + '@function': '0x0', + '@slot': '0x05', + '@type': 'pci'}}, + 'serial': {'@type': 'pty', 'target': {'@port': '0'}}, + 'video': {'address': {'@bus': '0x00', + '@domain': '0x0000', + '@function': '0x0', + '@slot': '0x02', + '@type': 'pci'}, + 'model': {'@heads': '1', '@type': 'cirrus', '@vram': '16384'}}}, + 'features': {'acpi': True, 'apic': True, 'pae': True}, + 'memory': {'#text': '2097152', '@unit': 'KiB'}, + 'name': 'trusty', + 'on_crash': 'restart', + 'on_poweroff': 'destroy', + 'on_reboot': 'restart', + 'os': {'boot': {'@dev': 'hd'}, + 'bootmenu': {'@enable': 'no'}, + 'type': {'#text': 'hvm', '@arch': 'x86_64', '@machine': 'pc-i440fx-2.3'}}, + 'resource': {'partition': '/machine'}, + 'title': 'Ubuntu 14.04', + 'uuid': 'e486003f-9f11-f7df-5ed4-cceddbf087ab', + 'vcpu': {'#text': '2', '@placement': 'static'}}, + 'memory': {'@snapshot': 'internal'}, + 'name': '1453929756', + 'parent': {'name': '1453929671'}, + 'state': 'running'} diff --git a/doc/source/index.rst b/doc/source/index.rst index 20a456a..eec99aa 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,6 +11,7 @@ Welcome to kvm's documentation! Overview API + Examples Indices and tables ================== diff --git a/kvm.py b/kvm.py deleted file mode 100644 index 5da5aec..0000000 --- a/kvm.py +++ /dev/null @@ -1,329 +0,0 @@ -"""This module allow to manage KVM hosts.""" - -import os -import re -import random -import string -import weakref -import unix -from lxml import etree - -import sys -SELF = sys.modules[__name__] - - -# Controls. -CONTROLS = {'parse': False} -unix.CONTROLS.update(CONTROLS) - -# Characters in generating strings. -_CHOICES = string.ascii_letters[:6] + string.digits - -RUNNING = 'running' -IDLE = 'idle' -PAUSED = 'paused' -SHUTDOWN = 'shutdown' -SHUTOFF = 'shut off' -CRASHED = 'crashed' -DYING = 'dying' -SUSPENDED = 'pmsuspended' - -MAPPING = {'hypervisor': {'version': {'type': 'dict'}, - 'sysinfo': {'type': 'dict'}, - 'nodeinfo': {'type': 'dict'}, - 'nodecpumap': {'type': 'dict'}, - 'nodecpustats': {'type': 'dict'}, - 'nodememstats': {'type': 'dict'}, - 'nodesuspend': {'type': 'none'}, - 'node_memory_tune': {'type': 'none'}, - 'capabilities': {'type': 'xml', - 'key': 'capabilities'}, - 'domcapabilities': {'type': 'xml', - 'key': 'domainCapabilities'}, - 'freecell': {'type': 'dict'}, - 'freepages': {'type': 'dict'}, - 'allocpages': {'type': 'none'}}, - 'domain': {'autostart': {'type': 'none'}, - 'inject-nmi': {'type': 'none'}, - 'desc': {'type': 'dict'}, - 'destroy': {'type': 'none'}, - 'blkinfo': {'type': 'dict', - 'cmd': 'domblkinfo'}, - 'display': {'type': 'str', - 'cmd': 'domdisplay'}, - 'info': {'type': 'dict', - 'cmd': 'dominfo'}, - 'uuid': {'type': 'str', - 'cmd': 'domuuid'}, - 'id': {'type': 'str', - 'cmd': 'domid'}, - 'name': {'type': 'str', - 'cmd': 'domname'}, - 'state': {'type': 'str', - 'cmd': 'domstate'}, - 'control': {'type': 'str', - 'cmd': 'domcontrol'}, - 'dumpxml': {'type': 'xml', - 'key': 'domain'}, - 'reboot': {'type': 'none'}, - 'reset': {'type': 'none'}, - 'screenshot': {'type': 'none'}, - 'shutdown': {'type': 'none'}, - 'start': {'type': 'none'}, - 'suspend': {'type': 'none'}, - 'resume': {'type': 'none'}, - 'ttyconsole': {'type': 'str'}, - 'undefine': {'type': 'none'}, - }} - - -# -# Functions for generating datas. -# -def gen_uuid(): - """Generate a random uuid.""" - return '-'.join((''.join([random.choice(_CHOICES) for _ in range(0, 8)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 12)]))) - - -def gen_mac(): - """Generate a random mac address.""" - return ':'.join(('54', '52', '00', - ''.join([random.choice(_CHOICES) for _ in range(0, 2)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 2)]), - ''.join([random.choice(_CHOICES) for _ in range(0, 2)]))) - - -def _xml_to_dict(elt): - """Recursive function that transform an XML element to a dictionnary. - **elt** must be of type ``lxml.etree.Element``.""" - tag = elt.tag - attrs = elt.items() - text = elt.text.strip() if elt.text else None - childs = elt.getchildren() - - if not attrs and not childs and not text: - return {tag: True} - elif not attrs and not childs and text: - return {tag: text} - elif attrs and not childs: - child = {'@%s' % attr: value for attr, value in attrs} - if text: - child['#text'] = text - return {tag: child} - elif childs: - elts = {'@%s' % attr: value for attr, value in attrs} if attrs else {} - for child in childs: - child = _xml_to_dict(child) - child_tag = list(child.keys())[0] - if child_tag in elts: - if not isinstance(elts[child_tag], list): - elts[child_tag] = [elts[child_tag]] - elts[child_tag].append(child[child_tag]) - else: - elts.update(child) - return {tag: elts} - - -def __str_to_dict(string): - def format_key(key): - return (key.strip().lower() - .replace(' ', '_').replace('(', '').replace(')', '')) - return {format_key(key): (value or '').strip() - for line in string if line - for key, value in [line.split(':')]} - - -def __add_method(obj, method, conf): - cmd = conf.get('cmd', method) - def str_method(self, *args, **kwargs): - with self._host.set_controls(parse=True): - return self._host.virsh(cmd, *args, **kwargs)[0] - - def dict_method(self, *args, **kwargs): - with self._host.set_controls(parse=True): - return __str_to_dict(self._host.virsh(cmd, *args, **kwargs)) - - def none_method(self, *args, **kwargs): - return self._host.virsh(method, *args, **kwargs) - - def xml_method(self, *args, **kwargs): - with self._host.set_controls(parse=True): - xml = '\n'.join(self._host.virsh(cmd, *args, **kwargs)) - return _xml_to_dict(etree.fromstring(xml))[conf['key']] - - setattr(obj, method, locals()['%s_method' % conf['type']]) - - -# -# Exceptions -# -class KvmError(Exception): - """Main exception for this module.""" - pass - - -class TimeoutException(Exception): - """Exception raise when a timeout is exceeded.""" - pass - - -# -## Classes. -# -def Hypervisor(host): - unix.isvalid(host) - - class Hypervisor(host.__class__): - """This object represent an Hypervisor. **host** must be an object of - type ``unix.Local`` or ``unix.Remote`` (or an object inheriting from - them). - """ - def __init__(self): - host.__class__.__init__(self) - self.__dict__.update(host.__dict__) - for control, value in CONTROLS.items(): - setattr(self, '_%s' % control, value) - - - def virsh(self, command, *args, **kwargs): - """Wrap the execution of the virsh command. It set a control for - putting options after the virsh **command**. If **parse** control - is activated, the value of ``stdout`` is returned or **KvmError** - exception is raised. - """ - with self.set_controls(options_place='after', decode='utf-8'): - status, stdout, stderr = self.execute('virsh', - command, - *args, - **kwargs) - # Clean stdout and stderr. - if stdout: - stdout = stdout.rstrip('\n') - if stderr: - stderr = stderr.rstrip('\n') - - if not self._parse: - return status, stdout, stderr - elif not status: - raise KvmError(stderr) - else: - stdout = stdout.splitlines() - return stdout[:-1] if not stdout[-1] else stdout - - - @property - def hypervisor(self): - return _Hypervisor(weakref.ref(self)()) - - - @property - def domain(self): - return _Domain(weakref.ref(self)()) - - - def list_domains(self, **kwargs): - """List domains. **kwargs** can contains any option supported by the - virsh command. It can also contains a **state** argument which is a - list of states for filtering (*all* option is automatically set). - For compatibility the options ``--table``, ``--name`` and ``--uuid`` - have been disabled. - - Virsh options are (some option may not work according your version): - * *all*: list all domains - * *inactive*: list only inactive domains - * *persistent*: include persistent domains - * *transient*: include transient domains - * *autostart*: list autostarting domains - * *no_autostart*: list not autostarting domains - * *with_snapshot*: list domains having snapshots - * *without_snapshort*: list domains not having snapshots - * *managed_save*: domains that have managed save state (only - possible if they are in the shut off state, - so you need to specify *inactive* or *all* - to actually list them) will instead show as - saved - * *with_managed_save*: list domains having a managed save image - * *without_managed_save*: list domains not having a managed - save image - """ - # Remove incompatible options between virsh versions. - kwargs.pop('name', None) - kwargs.pop('uuid', None) - - # Get states argument (which is not an option of the virsh command). - states = kwargs.pop('states', []) - if states: - kwargs['all'] = True - - # Add virsh options for kwargs. - virsh_opts = {arg: value for arg, value in kwargs.items() if value} - - # Get domains (filtered on state). - domains = {} - with self.set_controls(parse=True): - stdout = self.virsh('list', **virsh_opts) - elts = [elt.lower() for elt in stdout[0].split()] - for line in stdout[2:]: - values = [elt.strip() for elt in line.split(' ') if elt] - domain = dict(zip(elts, values)) - if states and domain['state'] not in states: - continue - domains.setdefault(domain.pop('name'), domain) - - return domains - - return Hypervisor() - - -class _Hypervisor(object): - def __init__(self, host): - self._host = host - -for mname, mconf in MAPPING['hypervisor'].items(): - __add_method(_Hypervisor, mname, mconf) - - -class _Domain(object): - def __init__(self, host): - self._host = host - - - def create(self, conf, *kwargs): - pass - - - def define(self, conf): - pass - - - def stop(self, domain, timeout=30, force=False): - import signal, time - - def timeout_handler(signum, frame): - raise TimeoutException() - - self.shutdown(domain) - old_handler = signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(timeout) - - try: - while self.state(domain) != SHUTOFF: - time.sleep(1) - except TimeoutException: - if force: - status, stdout, stderr = self.destroy(domain) - if status: - stderr = 'VM has been destroyed after %ss' % timeout - return (status, stdout, stderr) - else: - return (False, '', 'VM not stopped after %ss' % timeout) - finally: - signal.signal(signal.SIGALRM, old_handler) - signal.alarm(0) - -for mname, mconf in MAPPING['domain'].items(): - __add_method(_Domain, mname, mconf) diff --git a/kvm/__init__.py b/kvm/__init__.py new file mode 100644 index 0000000..453e6da --- /dev/null +++ b/kvm/__init__.py @@ -0,0 +1,575 @@ +"""This module allow to manage KVM hosts.""" + +import os +import re +import json +import random +import string +import weakref +import unix +import lxml.etree as etree +from collections import OrderedDict +from datetime import datetime + +import sys +_SELF = sys.modules[__name__] +_BUILTINS = sys.modules['builtins' + if sys.version_info.major == 3 + else '__builtin__'] + + +# Controls. +_CONTROLS = {'parse': False, 'ignore_opts': []} +unix._CONTROLS.update(_CONTROLS) + +# Characters in generating strings. +_CHOICES = string.ascii_letters[:6] + string.digits + +_ITEM_RE = re.compile('^.IX (?P\w+) "(?P.*)"$') + +__MAPFILE = os.path.join(os.path.dirname(__file__), 'kvm.json') +_MAPPING = json.loads(''.join([line + for line in open(__MAPFILE).readlines() + if not line.startswith('#')])) + +RUNNING = 'running' +IDLE = 'idle' +PAUSED = 'paused' +SHUTDOWN = 'shutdown' +SHUTOFF = 'shut off' +CRASHED = 'crashed' +DYING = 'dying' +SUSPENDED = 'pmsuspended' + + +def pprint(value): + return {key: pprint(val) if isinstance(val, (OrderedDict, dict)) + else [pprint(elt) for elt in val] + if isinstance(val, list) + else val + for key, val in value.items()} + +# +# Functions for generating datas. +# +def gen_uuid(): + """Generate a random uuid.""" + return '-'.join((''.join([random.choice(_CHOICES) for _ in range(0, 8)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 4)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 12)]))) + +def gen_mac(): + """Generate a random mac address.""" + return ':'.join(('54', '52', '00', + ''.join([random.choice(_CHOICES) for _ in range(0, 2)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 2)]), + ''.join([random.choice(_CHOICES) for _ in range(0, 2)]))) + +def from_xml(elt, force_lists=[]): + """Recursive function that transform an XML element to a dictionnary. + **elt** must be of type ``lxml.etree.Element``.""" + tag = elt.tag + attrs = elt.items() + text = elt.text.strip() if elt.text else None + childs = elt.getchildren() + + if not attrs and not childs and not text: + value = True + elif not attrs and not childs and text: + value = text + elif attrs and not childs: + child = {'@%s' % attr: value for attr, value in attrs} + if text: + child['#text'] = text + value = child + elif childs: + elts = (OrderedDict(('@%s' % attr, value) for attr, value in attrs) + if attrs else OrderedDict()) + for child in childs: + child = from_xml(child, force_lists) + child_tag = list(child.keys())[0] + if child_tag not in elts and child_tag in force_lists: + elts[child_tag] = [] + if child_tag in elts: + if not isinstance(elts[child_tag], list): + elts[child_tag] = [elts[child_tag]] + elts[child_tag].append(child[child_tag]) + else: + elts.update(child) + value = elts + + result = OrderedDict() + result[tag] = value + return result + +def to_xml(tag_name, conf): + def parse(tag_name, conf): + tag = etree.Element(tag_name) + for elt, value in conf.items(): + if elt.startswith('@'): + tag.attrib[elt[1:]] = str(value) + elif elt == '#text': + tag.text = str(value) + elif isinstance(value, dict): + tag.append(parse(elt, value)) + elif isinstance(value, list): + for child in value: + tag.append(parse(elt, child)) + elif isinstance(value, bool): + tag.append(etree.Element(elt)) + continue + else: + child = etree.Element(elt) + child.text = value + tag.append(child) + return tag + return etree.tostring(parse(tag_name, conf), pretty_print=True).decode() + +def _dict(lines): + def format_key(key): + return (key.strip().lower() + .replace(' ', '_').replace('(', '').replace(')', '')) + + elts = {} + for line in lines: + if not line: + continue + try: + key, value = line.split(':') + except ValueError: + try: + key, value = line.split() + except ValueError: + continue + elts[format_key(key)] = _convert(value or '') + return elts + +def _stats(lines, ignore=False): + return {elts[1 if ignore else 0]: elts[2 if ignore else 1] + for line in lines if line + for elts in [line.split()]} + +def _list(lines): + params = [param.lower() for param in re.split('\s+', lines[0])][1:] + return [dict(zip(params, re.split('\s+', line)[1:])) for line in lines[2:]] + +def __add_method(obj, method, conf): + cmd = conf.get('cmd', method) + ignore_opts = conf.pop('disable', []) + def str_method(self, *args, **kwargs): + with self._host.set_controls(parse=True, ignore_opts=ignore_opts): + result = self._host.virsh(cmd, *args, **kwargs)[0] + if 'convert' in conf: + try: + return getattr(_BUILTINS, conf['convert'])(result) + except ValueError: + return -1 if conf['convert'] == 'int' else result + return result + + def dict_method(self, *args, **kwargs): + with self._host.set_controls(parse=True, ignore_opts=ignore_opts): + return _dict(self._host.virsh(cmd, *args, **kwargs)) + + def stats_method(self, *args, **kwargs): + with self._host.set_controls(parse=True, ignore_opts=ignore_opts): + + return _stats(self._host.virsh(cmd, *args, **kwargs), + conf.get('ignore', False)) + + def list_method(self, *args, **kwargs): + with self._host.set_controls(parse=True, ignore_opts=ignore_opts): + return _list(self._host.virsh(cmd, *args, **kwargs)) + + def tune_method(self, *args, **kwargs): + ignore_opts = ('config', 'live', 'current') + if (not kwargs + or (len(kwargs) == 1 and any(opt in kwargs for opt in ignore_opts))): + return dict_method(self, *args, **kwargs) + else: + return none_method(self, *args, **kwargs) + + def none_method(self, *args, **kwargs): + return self._host.virsh(cmd, *args, **kwargs) + + def xml_method(self, *args, **kwargs): + with self._host.set_controls(parse=True, ignore_opts=ignore_opts): + xml = '\n'.join(self._host.virsh(cmd, *args, **kwargs)) + return from_xml(etree.fromstring(xml), conf.get('lists', []))[conf['key']] + + setattr(obj, method.replace('-', '_'), locals()['%s_method' % conf['type']]) + +def _convert(value): + value = value.strip() + if value.isdigit(): + return int(value) + for val, map_val in (('yes', True), ('no', False)): + if value == val: + return map_val + return value + + +# +# Exceptions +# +class KvmError(Exception): + """Main exception for this module.""" + pass + +class TimeoutException(Exception): + """Exception raise when a timeout is exceeded.""" + pass + + +# +## Classes. +# +def Hypervisor(host, uri=None): + unix.isvalid(host) + + try: + host.which('virsh') + except unix.UnixError: + raise KvmError("unable to find 'virsh' command, is this a KVM host?") + + class Hypervisor(host.__class__): + """This object represent an Hypervisor. **host** must be an object of + type ``unix.Local`` or ``unix.Remote`` (or an object inheriting from + them). + """ + def __init__(self): + host.__class__.__init__(self) + self.__dict__.update(host.__dict__) + for control, value in _CONTROLS.items(): + setattr(self, '_%s' % control, value) + + def virsh(self, command, *args, **kwargs): + """Wrap the execution of the virsh command. It set a control for + putting options after the virsh **command**. If **parse** control + is activated, the value of ``stdout`` is returned or **KvmError** + exception is raised. + """ + if self._ignore_opts: + for opt in self._ignore_opts: + kwargs.update({opt: False}) + + virsh_cmd = 'virsh --connect %s' % (uri or 'qemu:///session') + with self.set_controls(options_place='after', decode='utf-8'): + status, stdout, stderr = self.execute(virsh_cmd, command, *args, **kwargs) + # Clean stdout and stderr. + if stdout: + stdout = stdout.rstrip('\n') + if stderr: + stderr = stderr.rstrip('\n') + + if not self._parse: + return status, stdout, stderr + elif not status: + raise KvmError(stderr) + else: + stdout = stdout.splitlines() + return stdout[:-1] if not stdout[-1] else stdout + + def list_domains(self, **kwargs): + """List domains. **kwargs** can contains any option supported by the + virsh command. It can also contains a **state** argument which is a + list of states for filtering (*all* option is automatically set). + For compatibility the options ``--table``, ``--name`` and ``--uuid`` + have been disabled. + + Virsh options are (some option may not work according your version): + * *all*: list all domains + * *inactive*: list only inactive domains + * *persistent*: include persistent domains + * *transient*: include transient domains + * *autostart*: list autostarting domains + * *no_autostart*: list not autostarting domains + * *with_snapshot*: list domains having snapshots + * *without_snapshort*: list domains not having snapshots + * *managed_save*: domains that have managed save state (only + possible if they are in the shut off state, + so you need to specify *inactive* or *all* + to actually list them) will instead show as + saved + * *with_managed_save*: list domains having a managed save image + * *without_managed_save*: list domains not having a managed + save image + """ + # Remove incompatible options between virsh versions. + kwargs.pop('name', None) + kwargs.pop('uuid', None) + + # Get states argument (which is not an option of the virsh command). + states = kwargs.pop('states', []) + if states: + kwargs['all'] = True + + # Add virsh options for kwargs. + virsh_opts = {arg: value for arg, value in kwargs.items() if value} + + # Get domains (filtered on state). + domains = {} + with self.set_controls(parse=True): + stdout = self.virsh('list', **virsh_opts) + + for line in stdout[2:]: + line = line.split() + (domid, name, state), params = line[:3], line[3:] + # Manage state in two words. + if state == 'shut': + state += ' %s' % params.pop(0) + domain = {'id': int(domid) if domid != '-' else -1, + 'state': state} + if 'title' in kwargs: + domain['title'] = ' '.join(params) if params else '' + domains[name] = domain + + return domains + + def list_networks(self, **kwargs): + with self.set_controls(parse=True): + stdout = self.virsh('net-list', **kwargs) + networks = {} + for line in stdout[2:]: + line = line.split() + name, state, autostart = line[:3] + net = dict(state=state, autostart=_convert(autostart)) + if len(line) == 4: + net.update(persistent=_convert(line[3])) + networks.setdefault(name, net) + return networks + + def list_interfaces(self, **kwargs): + with self.set_controls(parse=True): + stdout = self.virsh('iface-list', **kwargs) + return {name: {'state': state, 'mac': mac} + for line in self.virsh('iface-list', **kwargs)[2:] + for name, state, mac in [line.split()]} + + def list_pools(self, **kwargs): + with self.set_controls(parse=True): + stdout = self.virsh('pool-list', **kwargs) + pools = {} + for line in stdout[2:]: + line = line.split() + name, state, autostart = line[:3] + pool = dict(state=state, autostart=_convert(autostart)) + if len(line) > 3: + pool.update(persistent=_convert(line[3]), + capacity=' '.join(line[4:6]), + allocation=' '.join(line[6:8]), + available=' '.join(line[8:10])) + pools.setdefault(line[0], pool) + return pools + + def list_volumes(self, pool, **kwargs): + with self.set_controls(parse=True): + stdout = self.virsh('vol-list', pool, **kwargs) + volumes = {} + for line in stdout[2:]: + line = line.split() + name, path = line[:2] + volume = dict(path=path) + if len(line) > 2: + volume.update(type=line[2], + capacity=' '.join(line[3:5]), + allocation=' '.join(line[5:7])) + volumes.setdefault(name, volume) + return volumes + + def list_secrets(self, **kwargs): + with self.set_controls(parse=True): + stdout = self.virsh('secret-list', **kwargs) + secrets = {} + for line in stdout[2:]: + line = line.split() + uuid, usage = line[0], line[1:] + secrets.setdefault(uuid, ' '.join(usage)) + return secrets + + def list_snapshots(self, domain, **kwargs): + kwargs.pop('tree', None) + kwargs.pop('name', None) + with self.set_controls(parse=True): + stdout = self.virsh('snapshot-list', domain, **kwargs) + snapshots = {} + for line in stdout[2:]: + line = line.split() + creation_date = datetime.strptime(' '.join(line[1:4]), + '%Y-%m-%d %H:%M:%S %z') + state = line[4] + if state == 'shut': + state += line[5] + parent = line[6] if 'parent' in kwargs else None + else: + parent = line[5] if 'parent' in kwargs else None + snapshot = {'creation_date': creation_date, 'state': state} + if parent and parent != 'null': + snapshot.update(parent=parent) + snapshots.setdefault(line[0], snapshot) + return snapshots + + @property + def image(self): + return _Image(weakref.ref(self)()) + + for property_name, property_methods in _MAPPING.items(): + property_obj = type('_%s' % str(property_name).capitalize(), + (object,), + dict(__init__=__init)) + + for method_name, method_conf in property_methods.items(): + __add_method(property_obj, method_name, method_conf) + # getattr(Hypervisor, method['name']).__doc__ = '\n'.join(method['doc']) + + for method_name in dir(_SELF): + if method_name.startswith('__%s' % property_name): + method = method_name.replace('__%s_' % property_name, '') + setattr(property_obj, method, getattr(_SELF, method_name)) + setattr(Hypervisor, property_name, property(property_obj)) + + return Hypervisor() + + +def __init(self, host): + self._host = host + +def __hypervisor_cpu_models(self, arch): + with self._host.set_controls(parse=True): + return self._host.virsh('cpu-models', arch) + +def __hypervisor_sysinfo(self): + entry = lambda value: {elt['@name']: elt['#text'] for elt in value} + + with self._host.set_controls(parse=True): + xml = '\n'.join(self._host.virsh('sysinfo')) + sysinfo = from_xml(etree.fromstring(xml))['sysinfo'] + result = {} + for elt, elt_entries in sysinfo.items(): + if elt.startswith('@'): + result[elt[1:]] = elt_entries + continue + result.update({elt: [entry(value['entry']) for value in elt_entries] + if isinstance(elt_entries, list) + else entry(elt_entries['entry'])}) + return result + +def __hypervisor_node_memory_tune(self, **kwargs): + if not kwargs: + with self._host.set_controls(parse=True): + return _dict(self._host.virsh('node-memory-tune')[1:]) + else: + return self._host.virsh('node-memory-tune', **kwargs) + +def __domain_time(self, domain, **kwargs): + kwargs.pop('pretty', None) + if not kwargs: + with self._host.set_controls(parse=True): + time = self._host.virsh('domtime', domain, **kwargs)[0] + return datetime.fromtimestamp(int(time.split(':')[1])) + else: + return self._host.virsh('domtime', domain, **kwargs) + +def __domain_cpustats(self, domain, **kwargs): + with self._host.set_controls(parse=True): + lines = self._host.virsh('cpu-stats', domain, **kwargs) + stats = {} + cur_cpu = '' + for line in lines: + if not line.startswith('\t'): + cur_cpu = line[:-1].lower() + stats.setdefault(cur_cpu, {}) + else: + param, value, unit = line[1:].split() + stats[cur_cpu][param] = '%s %s' % (value, unit) + return stats + +def __domain_stop(self, domain, timeout=30, force=False): + import signal, time + + def timeout_handler(signum, frame): + raise TimeoutException() + + # Check guest exists. + if domain not in self._host.list_domains(all=True): + return [False, '', 'Domain not found'] + + self.shutdown(domain) + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(timeout) + + try: + while self.state(domain) != SHUTOFF: + time.sleep(1) + except TimeoutException: + if force: + status, stdout, stderr = self.destroy(domain) + if status: + stderr = 'VM has been destroyed after %ss' % timeout + return (status, stdout, stderr) + else: + return (False, '', 'VM not stopped after %ss' % timeout) + finally: + signal.signal(signal.SIGALRM, old_handler) + signal.alarm(0) + return [True, '', ''] + +def __snapshot_current(self, domain, **kwargs): + with self._host.set_controls(parse=True): + result = self._host.virsh('snapshot-current', domain, **kwargs) + return (result[0] + if 'name' in kwargs + else from_xml(etree.fromstring('\n'.join(result)), [])['domainsnapshot']) + + +class _Image(object): + def __init__(self, host): + self._host = host + + def check(self, path, **kwargs): + return self._host.execute('qemu-img check', path, **kwargs) + + def create(self, path, size, **kwargs): + return self._host.execute('qemu-img create', path, size, **kwargs) + + def commit(self, path, **kwargs): + return self._host.execute('qemu-img commit', path, **kwargs) + + def compare(self, *paths, **kwargs): + return self._host.execute('qemu-img compare', *paths, **kwargs) + + def convert(self, src_path, dst_path, **kwargs): + with self._host.set_controls(options_place='after'): + return self._host.execute('qemu-img convert', src_path, dst_path, **kwargs) + + def info(self, path, **kwargs): + status, stdout, stderr = self._host.execute('qemu-img info', path, **kwargs) + if not status: + raise OSError(stderr) + return _dict(stdout.splitlines()) + + def map(self, path, **kwargs): + return self._host.execute('qemu-img map', path, **kwargs) + + def snapshot(self, path, **kwargs): + return self._host.execute('qemu-img snapshot', path, **kwargs) + + def rebase(self, path, **kwargs): + return self._host.execute('qemu-img rebase', path, **kwargs) + + def resize(self, path, size): + return self._host.execute('qemu-img resize', path, size) + + def amend(self, path, **kwargs): + return self._host.execute('qemu-img amend', path, **kwargs) + + def load(self, path, device='nbd0', **kwargs): + kwargs['c'] = '/dev/%s' % device + kwargs['d'] = False + return self._host.execute('qemu-nbd', path, **kwargs) + + def unload(self, device='nbd0', **kwargs): + kwargs['c'] = False + kwargs['d'] = '/dev/%s' % device + return self._host.execute('qemu-nbd', **kwargs) diff --git a/kvm/kvm.json b/kvm/kvm.json new file mode 100644 index 0000000..e5fb623 --- /dev/null +++ b/kvm/kvm.json @@ -0,0 +1,145 @@ +{"hypervisor": { + "version": {"cmd": "version", "type": "dict"}, + "uri": {"cmd": "uri", "type": "str"}, + "nodeinfo": {"cmd": "nodeinfo", "type": "dict"}, + "nodecpumap": {"cmd": "nodecpumap", "type": "dict"}, + "maxvcpus": {"cmd": "maxvcpus", "type": "str", "convert": "int"}, + "nodecpustats": {"cmd": "nodecpustats", "type": "dict"}, + "nodememstats": {"cmd": "nodememstats", "type": "dict"}, + "nodesuspend": {"cmd": "nodesuspend", "type": "none"}, + "capabilities": {"cmd": "capabilities", "type": "xml", "key": "capabilities"}, + "domcapabilities": {"cmd": "domcapabilities", "type": "xml", "key": "domainCapabilities"}, + "freecell": {"cmd": "freecell", "type": "dict"}, + "freepages": {"cmd": "freepages", "type": "dict"}, + "allocpages": {"cmd": "allocpages", "type": "none"}, + "cpu_baseline": {"cmd": "cpu-baseline", "type": "none"}, + "cpu_compare": {"cmd": "cpu-compare", "type": "none"}}, + "domain": { + "autostart": {"cmd": "autostart", "type": "none"}, + "inject_nmi": {"cmd": "inject_nmi", "type": "none"}, + "create": {"cmd": "create", "type": "none"}, + "define": {"cmd": "define", "type": "none"}, + "desc": {"cmd": "desc", "type": "dict"}, + "destroy": {"cmd": "destroy", "type": "none"}, + "blkstat": {"cmd": "domblkstat", "type": "stats", "ignore": true, "disable": ["human"]}, + "ifstat": {"cmd": "domifstat", "type": "stats", "ignore": true}, + "if_setlink": {"cmd": "domif-setlink", "type": "none"}, + "if_getlink": {"cmd": "domif-getlink", "type": "none"}, + "iftune": {"cmd": "domiftune", "type": "tune"}, + "memstat": {"cmd": "dommemstat", "type": "stats"}, + "blkinfo": {"cmd": "domblkinfo", "type": "dict"}, + "blklist": {"cmd": "domblklist", "type": "list"}, + "iflist": {"cmd": "domiflist", "type": "list"}, + "blkiotune": {"type": "tune"}, + "blkdeviotune": {"type": "tune"}, + "blockresize": {"type": "none"}, + "display": {"cmd": "domdisplay", "type": "str"}, + "info": {"cmd": "dominfo", "type": "dict"}, + "uuid": {"cmd": "domuuid", "type": "str"}, + "id": {"cmd": "domid", "type": "str", "convert": "int"}, + "name": {"cmd": "domname", "type": "str"}, + "state": {"cmd": "domstate", "type": "str"}, + "control": {"cmd": "domcontrol", "type": "str"}, + "coredump": {"cmd": "dump", "type": "none"}, + "conf": {"cmd": "dumpxml", "type": "xml", "key": "domain", "lists": ["disk", "interface"]}, + "managedsave": {"type": "none"}, + "managedsave_remove": {"cmd": "managedsave-remove", "type": "none"}, + "numatune": {"type": "tune"}, + "reboot": {"type": "none"}, + "reset": {"type": "none"}, + "save": {"type": "none"}, + "save_conf": {"cmd": "save-image-dumpxml", "type": "xml", "key": "domain", "lists": ["disk", "interface"]}, + "save_define": {"cmd": "save-image-define", "type": "none"}, + "restore": {"type": "none"}, + "schedinfo": {"type": "dict"}, + "screenshot": {"type": "none"}, + "send_key": {"cmd": "send-key", "type": "none"}, + "setmem": {"type": "none"}, + "setmaxmem": {"type": "none"}, + "memtune": {"type": "tune"}, + "setvcpus": {"type": "none"}, + "shutdown": {"type": "none"}, + "start": {"type": "none"}, + "suspend": {"type": "none"}, + "resume": {"type": "none"}, + "ttyconsole": {"type": "str"}, + "undefine": {"type": "none"}, + "pmsuspend": {"cmd": "dompmsuspend", "type": "none"}, + "pmwakeup": {"cmd": "dompmwakeup", "type": "none"}, + "attach_device": {"cmd": "attach-device", "type": "none"}, + "attach_disk": {"cmd": "attach-disk", "type": "none"}, + "attach_interface": {"cmd": "attach-interface", "type": "none"}, + "detach_device": {"cmd": "detach-device", "type": "none"}, + "detach_disk": {"cmd": "detach-disk", "type": "none"}, + "detach_interface": {"cmd": "detach-interface", "type": "none"}, + "update_device": {"cmd": "update-device", "type": "none"}}, + "network": { + "autostart": {"cmd": "net-autostart", "type": "none"}, + "create": {"cmd": "net-create", "type": "none"}, + "define": {"cmd": "net-define", "type": "none"}, + "destroy": {"cmd": "net-destroy", "type": "none"}, + "conf": {"cmd": "net-dumpxml", "type": "xml", "key": "network"}, + "info": {"cmd": "net-info", "type": "dict"}, + "name": {"cmd": "net-name", "type": "str"}, + "start": {"cmd": "net-start", "type": "none"}, + "undefine": {"cmd": "net-undefine", "type": "none"}, + "uuid": {"cmd": "net-uuid", "type": "str"}, + "update": {"cmd": "net-update", "type": "none"}}, + "interface": { + "bridge": {"cmd": "iface-bridge", "type": "none"}, + "define": {"cmd": "iface-define", "type": "none"}, + "destroy": {"cmd": "iface-destroy", "type": "none"}, + "conf": {"cmd": "iface-dumpxml", "type": "xml", "key": "interface"}, + "name": {"cmd": "iface-name", "type": "str"}, + "mac": {"cmd": "iface-mac", "type": "str"}, + "start": {"cmd": "iface-start", "type": "none"}, + "unbridge": {"cmd": "iface-unbridge", "type": "none"}, + "undefine": {"cmd": "iface-undefine", "type": "none"}, + "begin": {"cmd": "iface-begin", "type": "none"}, + "commit": {"cmd": "iface-commit", "type": "none"}, + "rollback": {"cmd": "iface-rollback", "type": "none"}}, + "pool": { + "autostart": {"cmd": "pool-autostart", "type": "none"}, + "build": {"cmd": "pool-build", "type": "none"}, + "create": {"cmd": "pool-create", "type": "none"}, + "create_as": {"cmd": "pool-create-as", "type": "none"}, + "define": {"cmd": "pool-define", "type": "none"}, + "define_as": {"cmd": "pool-define-as", "type": "none"}, + "destroy": {"cmd": "pool-destroy", "type": "none"}, + "delete": {"cmd": "pool-delete", "type": "none"}, + "conf": {"cmd": "pool-dumpxml", "type": "xml", "key": "pool"}, + "info": {"cmd": "pool-info", "type": "dict"}, + "name": {"cmd": "pool-name", "type": "str"}, + "refresh": {"cmd": "pool-refresh", "type": "none"}, + "start": {"cmd": "pool-start", "type": "none"}, + "undefine": {"cmd": "pool-undefine", "type": "none"}, + "uuid": {"cmd": "pool-uuid", "type": "str"}}, + "volume": { + "create": {"cmd": "vol-create", "type": "none"}, + "create_from": {"cmd": "vol-create-from", "type": "none"}, + "create_as": {"cmd": "vol-create-as", "type": "none"}, + "clone": {"cmd": "vol-clone", "type": "none"}, + "delete": {"cmd": "vol-delete", "type": "none"}, + "upload": {"cmd": "vol-upload", "type": "none"}, + "download": {"cmd": "vol-download", "type": "none"}, + "wipe": {"cmd": "vol-wipe", "type": "none"}, + "conf": {"cmd": "vol-dumpxml", "type": "xml", "key": "volume"}, + "info": {"cmd": "vol-info", "type": "dict"}, + "path": {"cmd": "vol-path", "type": "str"}, + "name": {"cmd": "vol-name", "type": "str"}, + "key": {"cmd": "vol-key", "type": "str"}, + "resize": {"cmd": "vol-resize", "type": "none"}}, + "secret": { + "define": {"cmd": "secret-define", "type": "none"}, + "conf": {"cmd": "secret-dumpxml", "type": "xml", "key": "secret"}, + "set_value": {"cmd": "secret-set-value", "type": "none"}, + "get_value": {"cmd": "secret-get-value", "type": "str"}, + "undefine": {"cmd": "secret-undefine", "type": "none"}}, + "snapshot": { + "create": {"cmd": "snapshot-create", "type": "none"}, + "create_as": {"cmd": "snapshot-create-as", "type": "none"}, + "info": {"cmd": "snapshot-info", "type": "dict"}, + "conf": {"cmd": "snapshot-dumpxml", "type": "xml", "key": "domainsnapshot"}, + "parent": {"cmd": "snapshot-parent", "type": "str"}, + "revert": {"cmd": "snapshot-revert", "type": "none"}, + "delete": {"cmd": "snapshot-delete", "type": "none"}}} diff --git a/setup.py b/setup.py index a1898dd..7755277 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,27 @@ # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup setup ( - name='Python remote KVM manager', - version='0.1', + name='kvm', + version='1.1.1', author='François Ménabé', author_email='francois.menabe@gmail.com', - py_modules=['kvm'], - licence='LICENCE.txt', + packages=['kvm'], + package_dir={'kvm': 'kvm'}, + package_data={'kvm': ['kvm.json']}, + license='MIT License', description='An API for managing KVM host.', - long_description=open('README.md').read(), - install_requires=[ - 'unix' - ], + long_description=open('README.rst').read(), + keywords = ['python', 'kvm', 'unix', 'virsh'], + install_requires=['unix', 'lxml'], +# entry_points={'unix': ['Hypervisor = kvm.__init__:Hypervisor']}, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Development Status :: 3 - Alpha', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Operating System :: Unix', + 'Topic :: System :: Systems Administration'] )