Adds basic XenServer client and models
* Models for VM, VBD, and VDI * Basic client calls for VM, VBD, and VDI records Change-Id: I942f146e17fa82ee87dd00444d38d04fad095f2b
This commit is contained in:
parent
d35b64ce02
commit
62b475f3ee
15
cloudcafe/compute/hypervisors/__init__.py
Normal file
15
cloudcafe/compute/hypervisors/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2013 Rackspace
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
15
cloudcafe/compute/hypervisors/xenserver/__init__.py
Normal file
15
cloudcafe/compute/hypervisors/xenserver/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2013 Rackspace
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
115
cloudcafe/compute/hypervisors/xenserver/client.py
Normal file
115
cloudcafe/compute/hypervisors/xenserver/client.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2013 Rackspace
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import XenAPI
|
||||||
|
|
||||||
|
from cafe.common.reporting import cclogging
|
||||||
|
from cafe.engine.clients.base import BaseClient
|
||||||
|
from cloudcafe.compute.hypervisors.xenserver.models.virtual_machine \
|
||||||
|
import VirtualMachine, VirtualBlockDevice, VirtualDiskImage
|
||||||
|
|
||||||
|
|
||||||
|
def _log_failure(log):
|
||||||
|
def _decorator(func):
|
||||||
|
def _wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
response = func(*args, **kwargs)
|
||||||
|
except XenAPI.Failure as exception:
|
||||||
|
log.error(exception)
|
||||||
|
raise exception
|
||||||
|
return response
|
||||||
|
return _wrapper
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
|
class XenAPIClient(BaseClient):
|
||||||
|
|
||||||
|
_log = cclogging.getLogger(__name__)
|
||||||
|
|
||||||
|
def __init__(self, url=None, username=None, password=None):
|
||||||
|
"""
|
||||||
|
Initialization
|
||||||
|
|
||||||
|
@param url: URL for the target XenServer instance
|
||||||
|
@type url: string
|
||||||
|
@param username: Username used to connect to XenServer
|
||||||
|
@type username: string
|
||||||
|
@param password: Password for the provided username
|
||||||
|
@type password: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.session = XenAPI.Session(url)
|
||||||
|
self.session.xenapi.login_with_password(username, password)
|
||||||
|
|
||||||
|
@_log_failure(log=_log)
|
||||||
|
def get_vm_record(self, server_id):
|
||||||
|
"""
|
||||||
|
Retrieves a VM Record for the given Compute uuid
|
||||||
|
|
||||||
|
@param server_id: The uuid of the Compute instance
|
||||||
|
@type server_id: string
|
||||||
|
@rtype: VirtualMachine
|
||||||
|
"""
|
||||||
|
vm = self._get_vm_by_compute_id(server_id)
|
||||||
|
record = self.session.xenapi.VM.get_record(vm)
|
||||||
|
virtual_machine = VirtualMachine._dict_to_obj(**record)
|
||||||
|
return virtual_machine
|
||||||
|
|
||||||
|
@_log_failure(log=_log)
|
||||||
|
def get_vbd_record(self, vbd):
|
||||||
|
"""
|
||||||
|
Retrieves a VBD Record
|
||||||
|
|
||||||
|
@param vbd: The OpaqueRef of the VBD
|
||||||
|
@type vbd: string
|
||||||
|
@rtype: VirtualBlockDevice
|
||||||
|
"""
|
||||||
|
|
||||||
|
record = self.session.xenapi.VBD.get_record(vbd)
|
||||||
|
block_device = VirtualBlockDevice._dict_to_obj(record)
|
||||||
|
return block_device
|
||||||
|
|
||||||
|
@_log_failure(log=_log)
|
||||||
|
def get_vdi_record(self, vdi):
|
||||||
|
"""
|
||||||
|
Retrieves a VDI Record
|
||||||
|
|
||||||
|
@param vdi: The OpaqueRef of the VDI
|
||||||
|
@type vdi: string
|
||||||
|
@rtype: VirtualDiskImage
|
||||||
|
"""
|
||||||
|
|
||||||
|
record = self.session.xenapi.VDI.get_record(vdi)
|
||||||
|
vdi = VirtualDiskImage._dict_to_obj(record)
|
||||||
|
return vdi
|
||||||
|
|
||||||
|
@_log_failure(log=_log)
|
||||||
|
def _get_vm_by_compute_id(self, server_id):
|
||||||
|
"""
|
||||||
|
Retrieves VM given a Compute uuid
|
||||||
|
|
||||||
|
@param server_id: The Compute uuid of an instance
|
||||||
|
@type server_id: string
|
||||||
|
@rtype: VM
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_vm_name = 'instance-{uuid}'.format(uuid=server_id)
|
||||||
|
virtual_machines = self.session.xenapi.VM.get_all()
|
||||||
|
for vm in virtual_machines:
|
||||||
|
vm_name = self.session.xenapi.VM.get_name_label(vm)
|
||||||
|
if expected_vm_name == vm_name:
|
||||||
|
return vm
|
||||||
|
return None
|
15
cloudcafe/compute/hypervisors/xenserver/models/__init__.py
Normal file
15
cloudcafe/compute/hypervisors/xenserver/models/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2013 Rackspace
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
@ -0,0 +1,256 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2013 Rackspace
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cafe.engine.models.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualMachine(BaseModel):
|
||||||
|
|
||||||
|
def __init__(self, label=None, description=None, vcpus_at_startup=None,
|
||||||
|
power_state=None, vcpus_params=None, vcpus_max=None,
|
||||||
|
xenstore_data=None, memory_static_min=None,
|
||||||
|
memory_static_max=None, memory_dynamic_min=None,
|
||||||
|
memory_dynamic_max=None, allowed_operations=None,
|
||||||
|
blocked_operations=None, ha_restart_priority=None,
|
||||||
|
pv_bootloader=None, snapshots=None, shutdown_delay=None,
|
||||||
|
domid=None, pci_bus=None, children=None,
|
||||||
|
hvm_shadow_multiplier=None, start_delay=None,
|
||||||
|
actions_after_crash=None, memory_target=None, uuid=None,
|
||||||
|
pv_ramdisk=None, tags=None, recommendations=None,
|
||||||
|
is_control_domain=None, hvm_boot_params=None,
|
||||||
|
snapshot_time=None, actions_after_shutdown=None,
|
||||||
|
user_version=None, snapshot_info=None,
|
||||||
|
transportable_snapshot_id=None, is_a_template=None,
|
||||||
|
crash_dumps=None, is_snapshot_from_vmpp=None,
|
||||||
|
is_a_snapshot=None, blobs=None, version=None,
|
||||||
|
current_operations=None, domarch=None,
|
||||||
|
pv_bootloader_args=None, snapshot_metadata=None,
|
||||||
|
other_config=None, actions_after_reboot=None,
|
||||||
|
attached_pcis=None, pv_legacy_args=None, bios_strings=None,
|
||||||
|
last_boot_cpu_flags=None, order=None):
|
||||||
|
super(VirtualMachine, self).__init__()
|
||||||
|
self.label = label
|
||||||
|
self.description = description
|
||||||
|
self.vcpus_at_startup = vcpus_at_startup
|
||||||
|
self.power_state = power_state
|
||||||
|
self.vcpus_params = vcpus_params
|
||||||
|
self.vcpus_max = vcpus_max
|
||||||
|
self.xenstore_data = xenstore_data
|
||||||
|
self.memory_static_min = memory_static_min
|
||||||
|
self.memory_static_max = memory_static_max
|
||||||
|
self.memory_dynamic_max = memory_dynamic_min
|
||||||
|
self.memory_dynamic_min = memory_dynamic_max
|
||||||
|
self.allowed_operations = allowed_operations
|
||||||
|
self.blocked_operations = blocked_operations
|
||||||
|
self.ha_restart_priority = ha_restart_priority
|
||||||
|
self.pv_bootloader = pv_bootloader
|
||||||
|
self.snapshots = snapshots
|
||||||
|
self.shutdown_delay = shutdown_delay
|
||||||
|
self.domid = domid
|
||||||
|
self.pci_bus = pci_bus
|
||||||
|
self.children = children
|
||||||
|
self.hvm_shadow_multiplier = hvm_shadow_multiplier
|
||||||
|
self.start_delay = start_delay
|
||||||
|
self.actions_after_crash = actions_after_crash
|
||||||
|
self.memory_target = memory_target
|
||||||
|
self.uuid = uuid
|
||||||
|
self.pv_ramdisk = pv_ramdisk
|
||||||
|
self.tags = tags
|
||||||
|
self.recommendations = recommendations
|
||||||
|
self.is_control_domain = is_control_domain
|
||||||
|
self.hvm_boot_params = hvm_boot_params
|
||||||
|
self.snapshot_time = snapshot_time
|
||||||
|
self.actions_after_shutdown = actions_after_shutdown
|
||||||
|
self.user_version = user_version
|
||||||
|
self.snapshot_info = snapshot_info
|
||||||
|
self.transportable_snapshot_id = transportable_snapshot_id
|
||||||
|
self.is_a_template = is_a_template
|
||||||
|
self.crash_dumps = crash_dumps
|
||||||
|
self.is_snapshot_from_vmpp = is_snapshot_from_vmpp
|
||||||
|
self.is_a_snapshot = is_a_snapshot
|
||||||
|
self.blobs = blobs
|
||||||
|
self.version = version
|
||||||
|
self.current_operations = current_operations
|
||||||
|
self.domarch = domarch
|
||||||
|
self.pv_bootloader_args = pv_bootloader_args
|
||||||
|
self.snapshot_metadata = snapshot_metadata
|
||||||
|
self.other_config = other_config
|
||||||
|
self.actions_after_reboot = actions_after_reboot
|
||||||
|
self.attached_pcis = attached_pcis
|
||||||
|
self.pv_legacy_args = pv_legacy_args
|
||||||
|
self.bios_strings = bios_strings
|
||||||
|
self.last_boot_cpu_flags = last_boot_cpu_flags
|
||||||
|
self.order = order
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _dict_to_obj(cls, **kwargs):
|
||||||
|
vm = VirtualMachine(
|
||||||
|
label=kwargs.get('name_label'),
|
||||||
|
description=kwargs.get('name_description'),
|
||||||
|
vcpus_at_startup=kwargs.get('VCPUs_at_startup'),
|
||||||
|
power_state=kwargs.get('power_state'),
|
||||||
|
vcpus_params=kwargs.get('vcpus_params'),
|
||||||
|
vcpus_max=kwargs.get('VCPUs_max'), version=kwargs.get('version'),
|
||||||
|
xenstore_data=kwargs.get('xenstore_data'),
|
||||||
|
memory_static_min=kwargs.get('memory_static_min'),
|
||||||
|
memory_static_max=kwargs.get('memory_static_max'),
|
||||||
|
memory_dynamic_min=kwargs.get('memory_dynamic_min'),
|
||||||
|
memory_dynamic_max=kwargs.get('memory_dynamic_max'),
|
||||||
|
allowed_operations=kwargs.get('allowed_operations'),
|
||||||
|
blocked_operations=kwargs.get('blocked_operations'),
|
||||||
|
ha_restart_priority=kwargs.get('ha_restart_priority'),
|
||||||
|
pv_bootloader=kwargs.get('PV_bootloader'),
|
||||||
|
snapshots=kwargs.get('snapshots'), domid=kwargs.get('domid'),
|
||||||
|
shutdown_delay=kwargs.get('shutdown_delay'),
|
||||||
|
pci_bus=kwargs.get('PCI_bus'), children=kwargs.get('children'),
|
||||||
|
hvm_shadow_multiplier=kwargs.get('HVM_shadow_multiplier'),
|
||||||
|
start_delay=kwargs.get('start_delay'), tags=kwargs.get('tags'),
|
||||||
|
actions_after_crash=kwargs.get('actions_after_crash'),
|
||||||
|
memory_target=kwargs.get('memory_target'),
|
||||||
|
uuid=kwargs.get('uuid'), pv_ramdisk=kwargs.get('pv_ramdisk'),
|
||||||
|
recommendations=kwargs.get('recommendations'),
|
||||||
|
is_control_domain=kwargs.get('is_control_domain'),
|
||||||
|
hvm_boot_params=kwargs.get('HVM_boot_params'),
|
||||||
|
snapshot_time=kwargs.get('snapshot_time'),
|
||||||
|
actions_after_shutdown=kwargs.get('actions_after_shutdown'),
|
||||||
|
user_version=kwargs.get('user_version'),
|
||||||
|
snapshot_info=kwargs.get('snapshot_info'),
|
||||||
|
transportable_snapshot_id=kwargs.get('transportable_snapshot_id'),
|
||||||
|
is_a_template=kwargs.get('is_a_template'),
|
||||||
|
crash_dumps=kwargs.get('crash_dumps'), blobs=kwargs.get('blobs'),
|
||||||
|
is_snapshot_from_vmpp=kwargs.get('is_snapshot_from_vmpp'),
|
||||||
|
is_a_snapshot=kwargs.get('is_a_snapshot'),
|
||||||
|
current_operations=kwargs.get('current_operations'),
|
||||||
|
domarch=kwargs.get('domarch'), order=kwargs.get('order'),
|
||||||
|
pv_bootloader_args=kwargs.get('PV_bootloader_args'),
|
||||||
|
snapshot_metadata=kwargs.get('snapshot_metadata'),
|
||||||
|
other_config=kwargs.get('other_config'),
|
||||||
|
actions_after_reboot=kwargs.get('actions_after_reboot'),
|
||||||
|
attached_pcis=kwargs.get('attached_PCIs'),
|
||||||
|
pv_legacy_args=kwargs.get('PV_legacy_args'),
|
||||||
|
bios_strings=kwargs.get('bios_strings'),
|
||||||
|
last_boot_cpu_flags=kwargs.get('last_boot_cpu_flags'))
|
||||||
|
return vm
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualBlockDevice(BaseModel):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, userdevice=None, runtime_properties=None,
|
||||||
|
allowed_operations=None, uuid=None, storage_lock=None,
|
||||||
|
qos_supported_algorithms=None, status_code=None,
|
||||||
|
type=None, empty=None, status_detail=None, device=None,
|
||||||
|
qos_algorithm_type=None, unpluggable=None,
|
||||||
|
current_operations=None, bootable=None, other_config=None,
|
||||||
|
currently_attached=None, mode=None, qos_algorithm_params=None):
|
||||||
|
super(VirtualBlockDevice, self).__init__()
|
||||||
|
self.userdevice = userdevice
|
||||||
|
self.runtime_properties = runtime_properties
|
||||||
|
self.allowed_operations = allowed_operations
|
||||||
|
self.uuid = uuid
|
||||||
|
self.storage_lock = storage_lock
|
||||||
|
self.qos_supported_algorithms = qos_supported_algorithms
|
||||||
|
self.status_code = status_code
|
||||||
|
self.type = type
|
||||||
|
self.empty = empty
|
||||||
|
self.status_detail = status_detail
|
||||||
|
self.device = device
|
||||||
|
self.qos_algorithm_type = qos_algorithm_type
|
||||||
|
self.unpluggable = unpluggable
|
||||||
|
self.current_operations = current_operations
|
||||||
|
self.bootable = bootable
|
||||||
|
self.other_config = other_config
|
||||||
|
self.currently_attached = currently_attached
|
||||||
|
self.mode = mode
|
||||||
|
self.qos_algorithm_params = qos_algorithm_params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _dict_to_obj(cls, **kwargs):
|
||||||
|
vbd = VirtualBlockDevice(
|
||||||
|
userdevice=kwargs.get('userdevice'),
|
||||||
|
runtime_properties=kwargs.get('runtime_properties'),
|
||||||
|
allowed_operations=kwargs.get('allowed_operations'),
|
||||||
|
uuid=kwargs.get('uuid'), storage_lock=kwargs.get('storage_lock'),
|
||||||
|
qos_supported_algorithms=kwargs.get('qos_supported_algorithms'),
|
||||||
|
status_code=kwargs.get('status_code'), type=kwargs.get('type'),
|
||||||
|
empty=kwargs.get('empty'), device=kwargs.get('device'),
|
||||||
|
status_detail=kwargs.get('status_detail'),
|
||||||
|
qos_algorithm_type=kwargs.get('qos_algorithm_type'),
|
||||||
|
unpluggable=kwargs.get('unpluggable'),
|
||||||
|
current_operations=kwargs.get('current_operations'),
|
||||||
|
bootable=kwargs.get('bootable'), mode=kwargs.get('mode'),
|
||||||
|
other_config=kwargs.get('other_config'),
|
||||||
|
currently_attached=kwargs.get('currently_attached'),
|
||||||
|
qos_algorithm_params=kwargs.get('qos_algorithm_params'))
|
||||||
|
return vbd
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualDiskImage(BaseModel):
|
||||||
|
|
||||||
|
def __init__(self, managed=None, snapshots=None, allowed_operations=None,
|
||||||
|
on_boot=None, description=None, read_only=None, uuid=None,
|
||||||
|
storage_lock=None, label=None, tags=None, location=None,
|
||||||
|
type=None, shareable=None, snapshot_time=None, missing=None,
|
||||||
|
xenstore_data=None, crash_dumps=None, virtual_size=None,
|
||||||
|
is_a_snapshot=None, current_operations=None,
|
||||||
|
physical_utilisation=None, allow_caching=None,
|
||||||
|
metadata_latest=None):
|
||||||
|
super(VirtualDiskImage, self).__init__()
|
||||||
|
self.managed = managed
|
||||||
|
self.snapshots = snapshots
|
||||||
|
self.allowed_operations = allowed_operations
|
||||||
|
self.on_boot = on_boot
|
||||||
|
self.description = description
|
||||||
|
self.read_only = read_only
|
||||||
|
self.uuid = uuid
|
||||||
|
self.storage_lock = storage_lock
|
||||||
|
self.label = label
|
||||||
|
self.tags = tags
|
||||||
|
self.location = location
|
||||||
|
self.type = type
|
||||||
|
self.shareable = shareable
|
||||||
|
self.snapshot_time = snapshot_time
|
||||||
|
self.missing = missing
|
||||||
|
self.xenstore_data = xenstore_data
|
||||||
|
self.crash_dumps = crash_dumps
|
||||||
|
self.virtual_size = virtual_size
|
||||||
|
self.is_a_snapshot = is_a_snapshot
|
||||||
|
self.current_operations = current_operations
|
||||||
|
self.physical_utilisation = physical_utilisation
|
||||||
|
self.allow_caching = allow_caching
|
||||||
|
self.metadata_latest = metadata_latest
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _dict_to_obj(cls, **kwargs):
|
||||||
|
vdi = VirtualDiskImage(
|
||||||
|
managed=kwargs.get('managed'), snapshots=kwargs.get('snapshots'),
|
||||||
|
allowed_operations=kwargs.get('allowed_operations'),
|
||||||
|
on_boot=kwargs.get('on_boot'), read_only=kwargs.get('read_only'),
|
||||||
|
description=kwargs.get('description'), uuid=kwargs.get('uuid'),
|
||||||
|
storage_lock=kwargs.get('storage_lock'), tags=kwargs.get('tags'),
|
||||||
|
label=kwargs.get('label'), location=kwargs.get('location'),
|
||||||
|
type=kwargs.get('type'), shareable=kwargs.get('shareable'),
|
||||||
|
snapshot_time=kwargs.get('snapshot_time'),
|
||||||
|
missing=kwargs.get('missing'),
|
||||||
|
virtual_size=kwargs.get('virtual_size'),
|
||||||
|
crash_dumps=kwargs.get('crash_dumps'),
|
||||||
|
xenstore_data=kwargs.get('xenstore_data'),
|
||||||
|
is_a_snapshot=kwargs.get('is_a_snapshot'),
|
||||||
|
current_operations=kwargs.get('current_operations'),
|
||||||
|
physical_utilisation=kwargs.get('physical_utilisation'),
|
||||||
|
allow_caching=kwargs.get('allow_caching'),
|
||||||
|
metadata_latest=kwargs.get('metadata_latest'))
|
||||||
|
return vdi
|
@ -2,3 +2,4 @@ httpretty
|
|||||||
mock
|
mock
|
||||||
unittest2
|
unittest2
|
||||||
IPy
|
IPy
|
||||||
|
XenAPI
|
Loading…
x
Reference in New Issue
Block a user