class CloudBlockStorageVolume(BaseResource): """ This class represents a Block Storage volume. """ def __init__(self, *args, **kwargs): super(CloudBlockStorageVolume, self).__init__(*args, **kwargs) try: self._nova_volumes = pyrax.cloudservers.volumes except AttributeError: # This will happen in unit testing, where the full pyrax # namespace is not exposed. In that situation, there is # no need for the reference anyway pass self._snapshot_manager = BaseManager(self.manager.api, resource_class=CloudBlockStorageSnapshot, response_key="snapshot", uri_base="snapshots") def attach_to_instance(self, instance, mountpoint): """ Attaches this volume to the cloud server instance at the specified mountpoint. This requires a call to the cloud servers API; it cannot be done directly. """ instance_id = _resolve_id(instance) resp = self._nova_volumes.create_server_volume(instance_id, self.id, mountpoint) # The response should be a volume reference to this volume. return resp.id == self.id def detach(self): """ Detaches this volume from any device it may be attached to. If it is not attached, nothing happens. """ attachments = self.attachments if not attachments: # Not attached; no error needed, just return return # A volume can only be attached to one device at a time, but for some # reason this is a list instead of a singular value att = attachments[0] instance_id = att["server_id"] attachment_id = att["id"] self._nova_volumes.delete_server_volume(instance_id, attachment_id) def delete(self, force=False): """ Volumes cannot be deleted if either a) they are attached to a device, or b) they have any snapshots. This method overrides the base delete() method to both better handle these failures, and also to offer a 'force' option. When 'force' is True, the volume is detached, and any dependent snapshots are deleted before calling the volume's delete. """ if force: self.detach() self.delete_all_snapshots() try: super(CloudBlockStorageVolume, self).delete() except exc.VolumeNotAvailable: # Notify the user? Record it somewhere? # For now, just re-raise raise def create_snapshot(self, name=None, description=None, force=False): """ Creates a snapshot of this volume, with an optional name and description. Normally snapshots will not happen if the volume is attached. To override this default behavior, pass force=True. """ name = name or "" description = description or "" # Note that passing in non-None values is required for the _create_body # method to distinguish between this and the request to create and # instance. try: snap = self._snapshot_manager.create(volume=self, name=name, description=description, force=force) except exc.BadRequest as e: msg = str(e) if "Invalid volume: must be available" in msg: # The volume for the snapshot was attached. raise exc.VolumeNotAvailable("Cannot create a snapshot from an " "attached volume. Detach the volume before trying again, " "or pass 'force=True' to the create_snapshot() call.") else: # Some other error raise except exc.ClientException as e: if e.code == 409: if "Request conflicts with in-progress" in str(e): raise exc.VolumeNotAvailable("The volume is current " "creating a snapshot. You must wait until that " "completes before attempting to create an " "additional snapshot.") else: raise else: raise return snap def list_snapshots(self): """ Returns a list of all snapshots of this volume. """ return [snap for snap in self._snapshot_manager.list() if snap.volume_id == self.id] def delete_all_snapshots(self): """ Locates all snapshots of this volume and deletes them. """ for snap in self.list_snapshots(): snap.delete() def _get_name(self): return self.display_name def _set_name(self, val): self.display_name = val name = property(_get_name, _set_name, None, "Convenience for referencing the display_name.") def _get_description(self): return self.display_description def _set_description(self, val): self.display_description = val description = property(_get_description, _set_description, None, "Convenience for referencing the display_description.")
class CloudBlockStorageVolume(BaseResource): """ This class represents a Block Storage volume. """ def __init__(self, *args, **kwargs): super(CloudBlockStorageVolume, self).__init__(*args, **kwargs) try: region = self.manager.api.region_name self._nova_volumes = pyrax.connect_to_cloudservers(region).volumes except AttributeError: # This will happen in unit testing, where the full pyrax # namespace is not exposed. In that situation, there is # no need for the reference anyway pass self._snapshot_manager = BaseManager(self.manager.api, resource_class=CloudBlockStorageSnapshot, response_key="snapshot", uri_base="snapshots") def attach_to_instance(self, instance, mountpoint): """ Attaches this volume to the cloud server instance at the specified mountpoint. This requires a call to the cloud servers API; it cannot be done directly. """ instance_id = _resolve_id(instance) try: resp = self._nova_volumes.create_server_volume(instance_id, self.id, mountpoint) except Exception as e: raise exc.VolumeAttachmentFailed("%s" % e) def detach(self): """ Detaches this volume from any device it may be attached to. If it is not attached, nothing happens. """ attachments = self.attachments if not attachments: # Not attached; no error needed, just return return # A volume can only be attached to one device at a time, but for some # reason this is a list instead of a singular value att = attachments[0] instance_id = att["server_id"] attachment_id = att["id"] try: self._nova_volumes.delete_server_volume(instance_id, attachment_id) except Exception as e: raise exc.VolumeDetachmentFailed("%s" % e) def delete(self, force=False): """ Volumes cannot be deleted if either a) they are attached to a device, or b) they have any snapshots. This method overrides the base delete() method to both better handle these failures, and also to offer a 'force' option. When 'force' is True, the volume is detached, and any dependent snapshots are deleted before calling the volume's delete. """ if force: self.detach() self.delete_all_snapshots() try: super(CloudBlockStorageVolume, self).delete() except exc.VolumeNotAvailable: # Notify the user? Record it somewhere? # For now, just re-raise raise def create_snapshot(self, name=None, description=None, force=False): """ Creates a snapshot of this volume, with an optional name and description. Normally snapshots will not happen if the volume is attached. To override this default behavior, pass force=True. """ name = name or "" description = description or "" # Note that passing in non-None values is required for the _create_body # method to distinguish between this and the request to create and # instance. try: snap = self._snapshot_manager.create(volume=self, name=name, description=description, force=force) except exc.BadRequest as e: msg = str(e) if "Invalid volume: must be available" in msg: # The volume for the snapshot was attached. raise exc.VolumeNotAvailable("Cannot create a snapshot from an " "attached volume. Detach the volume before trying again, " "or pass 'force=True' to the create_snapshot() call.") else: # Some other error raise except exc.ClientException as e: if e.code == 409: if "Request conflicts with in-progress" in str(e): raise exc.VolumeNotAvailable("The volume is current " "creating a snapshot. You must wait until that " "completes before attempting to create an " "additional snapshot.") else: raise else: raise return snap def list_snapshots(self): """ Returns a list of all snapshots of this volume. """ return [snap for snap in self._snapshot_manager.list() if snap.volume_id == self.id] def delete_all_snapshots(self): """ Locates all snapshots of this volume and deletes them. """ for snap in self.list_snapshots(): snap.delete() def _get_name(self): return self.display_name def _set_name(self, val): self.display_name = val name = property(_get_name, _set_name, None, "Convenience for referencing the display_name.") def _get_description(self): return self.display_description def _set_description(self, val): self.display_description = val description = property(_get_description, _set_description, None, "Convenience for referencing the display_description.")
class CloudDatabaseInstance(BaseResource): """ This class represents a MySQL instance in the cloud. """ def __init__(self, *args, **kwargs): super(CloudDatabaseInstance, self).__init__(*args, **kwargs) self._database_manager = BaseManager(self.manager.api, resource_class=CloudDatabaseDatabase, response_key="database", uri_base="instances/%s/databases" % self.id) self._user_manager = BaseManager(self.manager.api, resource_class=CloudDatabaseUser, response_key="user", uri_base="instances/%s/users" % self.id) def list_databases(self): """Returns a list of the names of all databases for this instance.""" return self._database_manager.list() def list_users(self): """Returns a list of the names of all users for this instance.""" return self._user_manager.list() def get_database(self, name): """ Finds the database in this instance with the specified name, and returns a CloudDatabaseDatabase object. If no match is found, a NoSuchDatabase exception is raised. """ try: return [db for db in self.list_databases() if db.name == name][0] except IndexError: raise exc.NoSuchDatabase("No database by the name '%s' exists." % name) def get_user(self, name): """ Finds the user in this instance with the specified name, and returns a CloudDatabaseUser object. If no match is found, a NoSuchDatabaseUser exception is raised. """ try: return [user for user in self.list_users() if user.name == name][0] except IndexError: raise exc.NoSuchDatabaseUser("No user by the name '%s' exists." % name) def create_database(self, name, character_set=None, collate=None): """ Creates a database with the specified name. If a database with that name already exists, a BadRequest (400) exception will be raised. """ if character_set is None: character_set = "utf8" if collate is None: collate = "utf8_general_ci" # Note that passing in non-None values is required for the _create_body # method to distinguish between this and the request to create and instance. self._database_manager.create(name=name, character_set=character_set, collate=collate, return_none=True) # Since the API doesn't return the info for creating the database object, we # have to do it manually. return self._database_manager.find(name=name) def create_user(self, name, password, database_names): """ Creates a user with the specified name and password, and gives that user access to the specified database(s). If a user with that name already exists, a BadRequest (400) exception will be raised. """ if not isinstance(database_names, list): database_names = [database_names] # The API only accepts names, not DB objects database_names = [db if isinstance(db, basestring) else db.name for db in database_names] # Note that passing in non-None values is required for the create_body # method to distinguish between this and the request to create and instance. self._user_manager.create(name=name, password=password, database_names=database_names, return_none=True) # Since the API doesn't return the info for creating the user object, we # have to do it manually. return self._user_manager.find(name=name) def _get_name(self, name_or_obj): """ For convenience, many methods accept either an object or the name of the object as a parameter, but need the name to send to the API. This method handles that conversion. """ if isinstance(name_or_obj, basestring): return name_or_obj try: return name_or_obj.name except AttributeError: msg = "The object '%s' does not have a 'name' attribute." % name_or_obj raise exc.MissingName(msg) def delete_database(self, name_or_obj): """ Deletes the specified database. If no database by that name exists, no exception will be raised; instead, nothing at all is done. """ name = self._get_name(name_or_obj) self._database_manager.delete(name) def delete_user(self, name_or_obj): """ Deletes the specified user. If no user by that name exists, no exception will be raised; instead, nothing at all is done. """ name = self._get_name(name_or_obj) self._user_manager.delete(name) def enable_root_user(self): """ Enables login from any host for the root user and provides the user with a generated root password. """ uri = "/instances/%s/root" % self.id resp, body = self.manager.api.method_post(uri) return body["user"]["password"] def root_user_status(self): """ Returns True or False, depending on whether the root user for this instance has been enabled. """ uri = "/instances/%s/root" % self.id resp, body = self.manager.api.method_get(uri) return body["rootEnabled"] def restart(self): """Restarts this instance.""" self.manager.action(self, "restart") def resize(self, flavor): """Set the size of this instance to a different flavor.""" # We need the flavorRef, not the flavor or size. flavorRef = self.manager.api._get_flavor_ref(flavor) body = {"flavorRef": flavorRef} self.manager.action(self, "resize", body=body) def resize_volume(self, size): """Changes the size of the volume for this instance.""" curr_size = self.volume.get("size") if size <= curr_size: raise exc.InvalidVolumeResize("The new volume size must be larger than the current volume size of '%s'." % curr_size) body = {"volume": {"size": size}} self.manager.action(self, "resize", body=body) def _get_flavor(self): try: ret = self._flavor except AttributeError: ret = self._flavor = CloudDatabaseFlavor(self.manager.api._flavor_manager, {}) return ret def _set_flavor(self, flavor): if isinstance(flavor, dict): self._flavor = CloudDatabaseFlavor(self.manager.api._flavor_manager, flavor) else: # Must be an instance self._flavor = flavor flavor = property(_get_flavor, _set_flavor)
class CloudDatabaseInstance(BaseResource): """ This class represents a MySQL instance in the cloud. """ def __init__(self, *args, **kwargs): super(CloudDatabaseInstance, self).__init__(*args, **kwargs) self._database_manager = BaseManager( self.manager.api, resource_class=CloudDatabaseDatabase, response_key="database", uri_base="instances/%s/databases" % self.id) self._user_manager = BaseManager(self.manager.api, resource_class=CloudDatabaseUser, response_key="user", uri_base="instances/%s/users" % self.id) # Remove the lazy load if not self.loaded: self.get() # Make the volume into an accessible object instead of a dict self.volume = CloudDatabaseVolume(self, self.volume) def list_databases(self): """Returns a list of the names of all databases for this instance.""" return self._database_manager.list() def list_users(self): """Returns a list of the names of all users for this instance.""" return self._user_manager.list() def get_database(self, name): """ Finds the database in this instance with the specified name, and returns a CloudDatabaseDatabase object. If no match is found, a NoSuchDatabase exception is raised. """ try: return [db for db in self.list_databases() if db.name == name][0] except IndexError: raise exc.NoSuchDatabase("No database by the name '%s' exists." % name) def get_user(self, name): """ Finds the user in this instance with the specified name, and returns a CloudDatabaseUser object. If no match is found, a NoSuchDatabaseUser exception is raised. """ try: return [user for user in self.list_users() if user.name == name][0] except IndexError: raise exc.NoSuchDatabaseUser("No user by the name '%s' exists." % name) def create_database(self, name, character_set=None, collate=None): """ Creates a database with the specified name. If a database with that name already exists, a BadRequest (400) exception will be raised. """ if character_set is None: character_set = "utf8" if collate is None: collate = "utf8_general_ci" # Note that passing in non-None values is required for the _create_body # method to distinguish between this and the request to create and # instance. self._database_manager.create(name=name, character_set=character_set, collate=collate, return_none=True) # Since the API doesn't return the info for creating the database # object, we have to do it manually. return self._database_manager.find(name=name) def create_user(self, name, password, database_names): """ Creates a user with the specified name and password, and gives that user access to the specified database(s). If a user with that name already exists, a BadRequest (400) exception will be raised. """ if not isinstance(database_names, list): database_names = [database_names] # The API only accepts names, not DB objects database_names = [ db if isinstance(db, basestring) else db.name for db in database_names ] # Note that passing in non-None values is required for the create_body # method to distinguish between this and the request to create and # instance. self._user_manager.create(name=name, password=password, database_names=database_names, return_none=True) # Since the API doesn't return the info for creating the user object, we # have to do it manually. return self._user_manager.find(name=name) def _get_name(self, name_or_obj): """ For convenience, many methods accept either an object or the name of the object as a parameter, but need the name to send to the API. This method handles that conversion. """ if isinstance(name_or_obj, basestring): return name_or_obj try: return name_or_obj.name except AttributeError: msg = "The object '%s' does not have a 'name' attribute." % name_or_obj raise exc.MissingName(msg) def delete_database(self, name_or_obj): """ Deletes the specified database. If no database by that name exists, no exception will be raised; instead, nothing at all is done. """ name = self._get_name(name_or_obj) self._database_manager.delete(name) def delete_user(self, name_or_obj): """ Deletes the specified user. If no user by that name exists, no exception will be raised; instead, nothing at all is done. """ name = self._get_name(name_or_obj) self._user_manager.delete(name) def enable_root_user(self): """ Enables login from any host for the root user and provides the user with a generated root password. """ uri = "/instances/%s/root" % self.id resp, body = self.manager.api.method_post(uri) return body["user"]["password"] def root_user_status(self): """ Returns True or False, depending on whether the root user for this instance has been enabled. """ uri = "/instances/%s/root" % self.id resp, body = self.manager.api.method_get(uri) return body["rootEnabled"] def restart(self): """Restarts this instance.""" self.manager.action(self, "restart") def resize(self, flavor): """Set the size of this instance to a different flavor.""" # We need the flavorRef, not the flavor or size. flavorRef = self.manager.api._get_flavor_ref(flavor) body = {"flavorRef": flavorRef} self.manager.action(self, "resize", body=body) def resize_volume(self, size): """Changes the size of the volume for this instance.""" curr_size = self.volume.size if size <= curr_size: raise exc.InvalidVolumeResize( "The new volume size must be larger " "than the current volume size of '%s'." % curr_size) body = {"volume": {"size": size}} self.manager.action(self, "resize", body=body) def _get_flavor(self): try: ret = self._flavor except AttributeError: ret = self._flavor = CloudDatabaseFlavor( self.manager.api._flavor_manager, {}) return ret def _set_flavor(self, flavor): if isinstance(flavor, dict): self._flavor = CloudDatabaseFlavor( self.manager.api._flavor_manager, flavor) else: # Must be an instance self._flavor = flavor flavor = property(_get_flavor, _set_flavor)