class ScaleIOConfig(object): """ScaleIO config manager.""" __scheme__ = { "network_timeout": Integer(min=0, optional=True), "request_retries": Integer(min=0, optional=True), "volume_prefix": String(optional=True), "volume_name": String(optional=True), } @classmethod def _get_scheme(cls): """Returns config scheme.""" return DictScheme(cls.__scheme__) def _validate(self, options): """Validates config.""" try: return validate("config", options, self._get_scheme()) except ValidationError as e: raise exceptions.ScaleIOConfigError(e) def apply(self, **options): """Applies config options to a config module.""" self._validate(options) for option, value in options.items(): setattr(pyscaleio.config, option.upper(), value)
class VTree(BaseResource): """Volume Tree (VTree) resource model.""" __scheme__ = { "name": String(optional=True), "baseVolumeId": String(), "storagePoolId": String(), } __parents__ = frozenset([ ("baseVolumeId", "Volume"), ("storagePoolId", "StoragePool") ])
class ProtectionDomain(MutableResource): """ProtectionDomain resource model.""" __scheme__ = { "name": String(), "systemId": String(), } __parents__ = frozenset([ ("systemId", "System") ]) @property def name(self): return self["name"]
def test_choices_invalid_value(): error = pytest.raises( InvalidValueError, lambda: _validate("c", String(choices=("a", "b")))).value assert error.object_name == "" assert error.object_value == "c"
def test_dict_invalid_key_scheme(): error = pytest.raises( InvalidTypeError, lambda: _validate({ True: "boolean", "string": "a", }, Dict(key_scheme=String()))).value assert error.object_name == "[True]" assert error.object_type == bool
def test_dict_invalid_value_scheme(): error = pytest.raises( InvalidTypeError, lambda: _validate({ False: 0, "string": "a", }, Dict(value_scheme=String()))).value assert error.object_name == "[False]" assert error.object_type == int
def test_dict_scheme_missing_parameter(): error = pytest.raises( MissingParameterError, lambda: _validate({ 2: "value", }, DictScheme({ 1: Bool(), 2: String() }))).value assert error.object_name == "[1]"
def test_model_validation_negative(client, modelklass): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String() }}) volume_payload = mock_resource_get("Volume", "test", {"id": "test"}) with httmock.HTTMock(login_payload, volume_payload): with pytest.raises(exceptions.ScaleIOValidationError) as e: klass("test") assert "instance['name'] is missing" in str(e)
def test_model_one(client, modelklass): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String(optional=True) }}) payload = {"id": "test", "name": "test_volume"} volume_payload = mock_resource_get("Volume", "test", payload) with httmock.HTTMock(login_payload, volume_payload): volume = klass.one("test") assert volume == payload
def test_dict_scheme_with_schema(): _validate({ False: "string", 1: True, "integer": 10, 4.4: 44.4, }, DictScheme({ False: String(), 1: Bool(), "integer": Integer(), 3.3: Float(optional=True), 4.4: Float(optional=True), }))
def test_dict_scheme_modification(): _validate_modification({ "1": "10", "2": "20", "3": "30" }, DictScheme({ "1": ToInt(), "2": String(), "3": ToInt() }), { "1": 10, "2": "20", "3": 30 })
class ExportsInfo(Sequence): """Information about volume exports.""" __scheme__ = List(DictScheme({ "sdcId": String(), "sdcIp": String(), "limitIops": Integer(), "limitBwInMbps": Integer() }), optional=True) def __init__(self, data=None): self._data = data or [] def __getitem__(self, index): return self._data[index] def __len__(self): return len(self._data) def __contains__(self, key): if isinstance(key, Sdc): return key["id"] in (e["sdcId"] for e in self._data) else: super(ExportsInfo, self).__contains__(key)
def test_model_validation(client, modelklass): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String() }}) volume_payload = mock_resource_get("Volume", "test", { "id": "test", "name": "test_volume" }) with httmock.HTTMock(login_payload, volume_payload): volume = klass("test") assert volume.get("id") == "test" assert volume.get("name") == "test_volume"
class System(EditableResource): """System resource model.""" __scheme__ = { "name": String(optional=True), "restrictedSdcModeEnabled": Bool(), } @property def name(self): return self.get("name") @property def is_restricted(self): return self["restrictedSdcModeEnabled"]
def test_model_all(client, modelklass): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String(optional=True) }}) payload = [{ "id": "test{0}".format(i), "name": "test_volume{0}".format(i) } for i in range(1, 2 + 1)] volumes_payload = mock_resources_get("Volume", payload) with httmock.HTTMock(login_payload, volumes_payload): volumes = klass.all() assert len(volumes) == 2 assert sorted(["test1", "test2"]) == sorted(v["id"] for v in volumes)
def test_model_update(client, modelklass, old_payload, new_payload): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String(optional=True) }}) volume_payload = mock_resource_get("Volume", "test", old_payload) volume_update_payload = mock_resource_get("Volume", "test", new_payload) with httmock.HTTMock(login_payload, volume_payload): volume = klass("test") assert volume == old_payload with httmock.HTTMock(volume_update_payload): volume.update() assert volume == new_payload
def test_custom_model_scheme(client, modelklass, scheme, data): base_scheme = {"id": String()} full_scheme = base_scheme full_scheme.update(scheme) data = data data.update({"id": "test_id"}) with mock.patch("pyscaleio.models.BaseResource.__scheme__", base_scheme): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": scheme}) result = klass._get_scheme() assert isinstance(result, DictScheme) assert result._DictScheme__scheme == full_scheme result = klass(instance=data)._get_scheme() assert isinstance(result, DictScheme) assert result._DictScheme__scheme == full_scheme
def test_model_all_by_ids(client, modelklass): klass = modelklass("Volume", (BaseResource, ), {"__scheme__": { "name": String(optional=True) }}) payload = [{ "id": "test{0}".format(i), "name": "test_volume{0}".format(i) } for i in range(1, 3 + 1)] def mocked_action(name, action, args): return [item for item in payload if item["id"] in args["ids"]] with mock.patch("pyscaleio.ScaleIOClient.perform_action_on_type", side_effect=mocked_action): volumes = klass.all(instance_ids=["test2", "test3"]) assert len(volumes) == 2 volumes = klass.all(instance_ids="test2") assert len(volumes) == 1 assert volumes[0]["id"] == "test2"
def test_dict_key_value(): _validate({ "one": 1.0, "two": 2.0, }, Dict(String(), Float()))
def test_string_regex_invalid(): with pytest.raises(InvalidValueError): _validate("12345", String(regex=r"^\d{4}$"))
def test_string_regex_valid(): _validate("12345", String(regex=r"^\d{5}$"))
def test_string(): _validate("string", String())
def test_choices(): _validate("b", String(choices=("a", "b")))
class Volume(MutableResource): """Volume resource model.""" __scheme__ = { "name": String(optional=True), "mappedSdcInfo": ExportsInfo.__scheme__, "useRmcache": Bool(), "sizeInKb": Integer(), "storagePoolId": String(), "volumeType": String(choices=constants.VOLUME_TYPES), "ancestorVolumeId": String(optional=True), } __parents__ = frozenset([ ("ancestorVolumeId", "Volume"), ("storagePoolId", "StoragePool"), ("vtreeId", "VTree"), ]) @pyscaleio.inject @classmethod def one_by_name(cls, client, name, **kwargs): """Returns volume instance by name. :param name: volume name (required) :rtype: pyscaleio.Volume """ volume_id = client.perform_action_on_type( cls._get_name(), "queryIdByKey", {"name": name}) return cls(volume_id, **kwargs) @classmethod def create(cls, size, pool, name=None, rmcache=None, thin=True, **kwargs): """Creates Volume instance. :param size: volume size in GB (required) :param pool: storage pool id (required) :param name: volume name :param thin: is volume 'thin' or 'thick' provisioned :param rmcache: volume rmcache :rtype: pyscaleio.Volume """ volume_size = (size * constants.GIGABYTE) // constants.KILOBYTE volume = { "volumeSizeInKb": str(volume_size), "storagePoolId": pool } if name: volume["name"] = name if rmcache: volume["useRmcache"] = rmcache volume["volumeType"] = constants.VOLUME_TYPE_THICK if thin: volume["volumeType"] = constants.VOLUME_TYPE_THIN return super(Volume, cls).create(volume, **kwargs) @property def name(self): return self.get("name") @property def size(self): return self["sizeInKb"] * constants.KILOBYTE @property def type(self): return self.get("volumeType") @property def exports(self): return ExportsInfo(self.get("mappedSdcInfo")) @property def path(self): device_name = config.VOLUME_NAME.format( system_id=self._client.system["id"], volume_id=self["id"] ) return os.path.join(config.VOLUME_PREFIX, device_name) def rename(self, name): """Changes volume name. :param name: new volume name """ return super(Volume, self).perform("setVolumeName", {"newName": name}) def resize(self, size): """Changes volumes size. :param size: new volume size in GB (required) """ return super(Volume, self).perform("setVolumeSize", {"sizeInGB": str(size)}) def snapshot(self, name=None): """Creates snapshot of current volume. :param name: snapshot name """ snapshot = {"volumeId": self["id"]} if name: snapshot["snapshotName"] = name result = self._client.system.perform( "snapshotVolumes", {"snapshotDefs": [snapshot]}) return Volume(result["volumeIdList"][0]) def throttle(self, sdc_id=None, sdc_guid=None, iops=None, mbps=None): """Throttles I/O on current volume. :param iops: I/O operations in seconds (> 10) :param mbps: network bandwidth as megabytes in seconds """ if sdc_id and sdc_guid: raise exceptions.ScaleIONotBothParameters("sdc_id", "sdc_guid") if iops is None and mbps is None: raise exceptions.ScaleIORequiredParameters("iops", "mbps") if iops is not None and iops != 0 and iops <= 10: raise exceptions.ScaleIOInvalidLimit("iops", "must be greater than 10.") if mbps is not None: mbps = mbps * constants.KILOBYTE if bool(mbps % constants.KILOBYTE): raise exceptions.ScaleIOInvalidLimit("mbps", "must be granular to 1024 Kbps") data = {} if sdc_id: data["sdcId"] = sdc_id if sdc_guid: data["guid"] = sdc_guid if iops is not None: data["iopsLimit"] = str(iops) if mbps is not None: data["bandwidthLimitInKbps"] = str(mbps) return super(Volume, self).perform("setMappedSdcLimits", data) def export(self, sdc_id=None, sdc_guid=None, multiple=False): """Exports volume to specified SDC. :param sdc_id: id of SDC instance :param sdc_guid: guid of SDC instance :param multiple: allows export to multiple SDCs (optional) """ if sdc_id and sdc_guid: raise exceptions.ScaleIONotBothParameters("sdc_id", "sdc_guid") data = {} if sdc_id: data["sdcId"] = sdc_id if sdc_guid: data["guid"] = sdc_guid if multiple: data["allowMultipleMappings"] = utils.bool_to_str(multiple) return super(Volume, self).perform("addMappedSdc", data) def unexport(self, sdc_id=None, sdc_guid=None): """Unexports volume from specified SDC. Without parameters unexports volume from all SDCs :param sdc_id: id of SDC instance :param sdc_guid: guid if SDC instance """ if sdc_id and sdc_guid: raise exceptions.ScaleIONotBothParameters("sdc_id", "sdc_guid") data = {} if sdc_id: data["sdcId"] = sdc_id elif sdc_guid: data["guid"] = sdc_guid else: data["allSdcs"] = "" return super(Volume, self).perform("removeMappedSdc", data) def delete(self, mode=constants.VOLUME_REMOVE_ONLY_ME): """Removes volume with specified mode. :param mode: volume remove mode (optional) """ return super(Volume, self).delete({"removeMode": mode})
def test_string_invalid_type(): error = pytest.raises(InvalidTypeError, lambda: _validate(b"bytes", String())).value assert error.object_name == "" assert error.object_type == bytes
@pytest.mark.parametrize(("name", "result"), [ ("Volume", "Volume"), ("VTree", "VTree"), ("StoragePool", "StoragePool"), ("Sdc", "Sdc"), ]) def test_custom_model_name(client, modelklass, name, result): assert BaseResource.__resource__ is None with mock.patch("pyscaleio.models.BaseResource.__scheme__", {}): klass = modelklass(name, (BaseResource, ), {"__scheme__": {}}) assert klass._get_name() == result assert klass(instance={})._get_name() == result @pytest.mark.parametrize("scheme", [{}, {"id": String()}]) def test_base_model_scheme(client, modelklass, scheme): with mock.patch("pyscaleio.models.BaseResource.__scheme__", scheme): result = BaseResource._get_scheme() assert isinstance(result, DictScheme) assert result._DictScheme__scheme == scheme result = BaseResource(instance={"id": "test"})._get_scheme() assert isinstance(result, DictScheme) assert result._DictScheme__scheme == scheme @pytest.mark.parametrize(("scheme", "data"), [({ "name": String()
class Sdc(MutableResource): """SDC resource model.""" __scheme__ = { "name": String(optional=True), "sdcIp": String(), "sdcGuid": String(), "sdcApproved": Bool(), "mdmConnectionState": String( choices=constants.SDC_MDM_STATES), } __parents__ = frozenset([ ("systemId", "System") ]) @pyscaleio.inject @classmethod def all_approved(cls, client, **kwargs): """Returns list of all approved SDCs.""" instances = client.perform_action_on_type( cls._get_name(), "queryAllApprovedSdc", {}) return [cls(instance=instance, client=client) for instance in instances ] @pyscaleio.inject @classmethod def one_by_ip(cls, client, ip_address, **kwargs): """Returns SDC instance by specified IP address. :param ip_address: IP address of SDC :rtype: pyscaleio.SDC """ instance_id = client.perform_action_on_type( cls._get_name(), "queryIdByKey", {"ip": ip_address}) return cls(instance_id, client=client) @property def name(self): return self.get("name") @property def ip(self): return self["sdcIp"] @property def guid(self): return self["sdcGuid"] @property def is_approved(self): return self["sdcApproved"] @property def is_connected(self): return self["mdmConnectionState"] == constants.SDC_MDM_STATE_CONNECTED
def test_string_max_length_invalid(): with pytest.raises(InvalidValueError): _validate("12345", String(max_length=4))
class StoragePool(MutableResource): """StoragePool resource model.""" __scheme__ = { "name": String(optional=True), "protectionDomainId": String(), "checksumEnabled": Bool(), "useRfcache": Bool(), } __parents__ = frozenset([ ("protectionDomainId", "ProtectionDomain") ]) @pyscaleio.inject @classmethod def one_by_name(cls, client, name, domain_name): """Returns StoragePool instance by name and protection domain name. :param name: storage pool name :param domain_name: protection domain name :rtype: pyscaleio.StoragePool """ data = { "name": name, "protectionDomainName": domain_name } pool_id = client.perform_action_on_type( cls._get_name(), "queryIdByKey", data) return cls(pool_id, client=client) @pyscaleio.inject @classmethod def create(cls, client, domain, checksum=False, rfcache=False, name=None, **kwargs): """Creates StoragePool instance. :param domain: protection domain id (required) :param checksum: enable checksum protection (default is False) :param rfcache: use read-only flash cache (default is False) :rtype: pyscaleio.StoragePool """ pool = { "protectionDomainId": domain, "checksumEnabled": utils.bool_to_str(checksum), "useRfcache": utils.bool_to_str(rfcache), } if name: pool["name"] = name return super(StoragePool, cls).create(pool, **kwargs) @property def name(self): return self.get("name") @property def checksum_enabled(self): return self["checksumEnabled"] @property def rfcache_enabled(self): return self["useRfcache"] def create_volume(self, size, **kwargs): """Creates Volume instance in current StoragePool. :param size: volume size in GB (required) :rtype: pyscaleio.Volume """ kwargs["client"] = self._client return Volume.create(size, self["id"], **kwargs)
class BaseResource(Mapping): """Base resource model.""" __scheme__ = { "id": String(), "links": List( DictScheme({ "href": String(), "rel": String() }), optional=True ) } """Data scheme for instance validation.""" __parents__ = None """ References to parent resources by fields. Example: frozenset([ ("parentField", "ResourceClassName") ]) """ __resource__ = None """ Custom resource name that overrides default name based on name of resource class. """ @classmethod def _get_name(cls): """Returns resource name. Attention: for internal use only! """ resource = cls.__resource__ if not resource: resource = camelize(underscore(cls.__name__)) return resource @classmethod def _get_scheme(cls): """Returns resource scheme for proper validation. Attention: for internal use only! """ scheme = {} for base in cls.mro(): if bool( issubclass(base, BaseResource) and getattr(base, "__scheme__", None) ): scheme.update(base.__scheme__) return DictScheme(scheme, ignore_unknown=True) @classmethod def one(cls, instance_id, **kwargs): """Returns instance of resource. :param instance_id: id of resource instance :returns: instance of resource """ return cls(instance_id, **kwargs) @pyscaleio.inject @classmethod def all(cls, client, instance_ids=None, **kwargs): """Returns list of resource instances. :param instance_ids: list of instance ids (optional) :returns: list of resource instances """ if not instance_ids: instances = client.get_instances_of(cls._get_name()) else: if isinstance(instance_ids, string_types): instance_ids = (instance_ids,) instances = client.perform_action_on_type( cls._get_name(), "queryBySelectedIds", {"ids": instance_ids}) return [cls(instance=instance, client=client) for instance in instances ] @pyscaleio.inject def __init__(self, client, instance_id=None, instance=None): self._client = client self._scheme = {} if instance_id and instance: raise exceptions.ScaleIONotBothParameters("instance_id", "instance") if instance_id: instance = self._client.get_instance_of(self._get_name(), instance_id) self._instance = self._validate(instance or {}) def __getitem__(self, key): return self._instance[key] def __iter__(self): return iter(self._instance) def __len__(self): return len(self._instance) @property def links(self): return self["links"] def _validate(self, instance): """Validates the instance if resource according to scheme. Attention: for internal use only! """ try: return validate("instance", instance, self._get_scheme()) except ValidationError as e: raise exceptions.ScaleIOValidationError(e) def update(self): """Updates resource instance.""" instance = self._client.get_instance_of(self._get_name(), self["id"]) instance = self._validate(instance) fields = set(list(instance) + list(self._instance)) for field in fields: try: instance[field] except KeyError: del self._instance[field] else: self._instance[field] = instance[field]