Repository: linode/linode_api4-python

Issue:
NodebalancerNode not found.
Hi, i was testing this, as describe in the documentation.

```
import linode

nb_node = linode.NodeBalancerNode(client, xxx, xxx, xxxx) # linode_client, node_id, config_id, nodebalancer_id
nb_node.weight = '100'
nb_node.save()
```

It returned this error:

Traceback (most recent call last):
  File "linode_script.py", line 26, in <module>
    nb_node = linode.NodeBalancerNode(client, xxx, xxx, xxxx)  # linode_client, node_id, config_id, nodebalancer_id
AttributeError: module 'linode' has no attribute 'NodeBalancerNode'

What am i doing wrong?

Thanks.

Discussion:

Patch:
diff --git a/linode_api4/groups/__init__.py b/linode_api4/groups/__init__.py
index 74419a6..bf815ff 100644
--- a/linode_api4/groups/__init__.py
+++ b/linode_api4/groups/__init__.py
@@ -10,7 +10,7 @@ from .lke import *
 from .longview import *
 from .networking import *
 from .nodebalancer import *
-from .obj import *
+from .object_storage import *
 from .profile import *
 from .region import *
 from .support import *
diff --git a/linode_api4/groups/database.py b/linode_api4/groups/database.py
index 494c30e..af4a7a8 100644
--- a/linode_api4/groups/database.py
+++ b/linode_api4/groups/database.py
@@ -5,7 +5,6 @@ from linode_api4.objects import (
     Database,
     DatabaseEngine,
     DatabaseType,
-    MongoDBDatabase,
     MySQLDatabase,
     PostgreSQLDatabase,
 )
@@ -200,67 +199,3 @@ class DatabaseGroup(Group):
 
         d = PostgreSQLDatabase(self.client, result["id"], result)
         return d
-
-    def mongodb_instances(self, *filters):
-        """
-        Returns a list of Managed MongoDB Databases active on this account.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-databases-list
-
-        :param filters: Any number of filters to apply to this query.
-
-        :returns: A list of MongoDB databases that matched the query.
-        :rtype: PaginatedList of MongoDBDatabase
-        """
-        return self.client._get_and_filter(MongoDBDatabase, *filters)
-
-    def mongodb_create(self, label, region, engine, ltype, **kwargs):
-        """
-        Creates an :any:`MongoDBDatabase` on this account with
-        the given label, region, engine, and node type.  For example::
-
-           client = LinodeClient(TOKEN)
-
-           # look up Region and Types to use.  In this example I'm just using
-           # the first ones returned.
-           region = client.regions().first()
-           node_type = client.database.types()[0]
-           engine = client.database.engines(DatabaseEngine.engine == 'mongodb')[0]
-
-           new_database = client.database.mongodb_create(
-               "example-database",
-               region,
-               engine.id,
-               type.id
-            )
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-create
-
-        :param label: The name for this cluster
-        :type label: str
-        :param region: The region to deploy this cluster in
-        :type region: str or Region
-        :param engine: The engine to deploy this cluster with
-        :type engine: str or Engine
-        :param ltype: The Linode Type to use for this cluster
-        :type ltype: str or Type
-        """
-
-        params = {
-            "label": label,
-            "region": region.id if issubclass(type(region), Base) else region,
-            "engine": engine.id if issubclass(type(engine), Base) else engine,
-            "type": ltype.id if issubclass(type(ltype), Base) else ltype,
-        }
-        params.update(kwargs)
-
-        result = self.client.post("/databases/mongodb/instances", data=params)
-
-        if "id" not in result:
-            raise UnexpectedResponseError(
-                "Unexpected response when creating MongoDB Database",
-                json=result,
-            )
-
-        d = MongoDBDatabase(self.client, result["id"], result)
-        return d
diff --git a/linode_api4/groups/object_storage.py b/linode_api4/groups/object_storage.py
new file mode 100644
index 0000000..514e5e3
--- /dev/null
+++ b/linode_api4/groups/object_storage.py
@@ -0,0 +1,333 @@
+from linode_api4.errors import UnexpectedResponseError
+from linode_api4.groups import Group
+from linode_api4.objects import (
+    Base,
+    MappedObject,
+    ObjectStorageACL,
+    ObjectStorageBucket,
+    ObjectStorageCluster,
+    ObjectStorageKeys,
+)
+from linode_api4.util import drop_null_keys
+
+
+class ObjectStorageGroup(Group):
+    """
+    This group encapsulates all endpoints under /object-storage, including viewing
+    available clusters, buckets, and managing keys and TLS/SSL certs, etc.
+    """
+
+    def clusters(self, *filters):
+        """
+        Returns a list of available Object Storage Clusters.  You may filter
+        this query to return only Clusters that are available in a specific region::
+
+           us_east_clusters = client.object_storage.clusters(ObjectStorageCluster.region == "us-east")
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#clusters-list
+
+        :param filters: Any number of filters to apply to this query.
+
+        :returns: A list of Object Storage Clusters that matched the query.
+        :rtype: PaginatedList of ObjectStorageCluster
+        """
+        return self.client._get_and_filter(ObjectStorageCluster, *filters)
+
+    def keys(self, *filters):
+        """
+        Returns a list of Object Storage Keys active on this account.  These keys
+        allow third-party applications to interact directly with Linode Object Storage.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-keys-list
+
+        :param filters: Any number of filters to apply to this query.
+
+        :returns: A list of Object Storage Keys that matched the query.
+        :rtype: PaginatedList of ObjectStorageKeys
+        """
+        return self.client._get_and_filter(ObjectStorageKeys, *filters)
+
+    def keys_create(self, label, bucket_access=None):
+        """
+        Creates a new Object Storage keypair that may be used to interact directly
+        with Linode Object Storage in third-party applications.  This response is
+        the only time that "secret_key" will be populated - be sure to capture its
+        value or it will be lost forever.
+
+        If given, `bucket_access` will cause the new keys to be restricted to only
+        the specified level of access for the specified buckets.  For example, to
+        create a keypair that can only access the "example" bucket in all clusters
+        (and assuming you own that bucket in every cluster), you might do this::
+
+           client = LinodeClient(TOKEN)
+
+           # look up clusters
+           all_clusters = client.object_storage.clusters()
+
+           new_keys = client.object_storage.keys_create(
+               "restricted-keys",
+               bucket_access=[
+                   client.object_storage.bucket_access(cluster, "example", "read_write")
+                   for cluster in all_clusters
+               ],
+           )
+
+        To create a keypair that can only read from the bucket "example2" in the
+        "us-east-1" cluster (an assuming you own that bucket in that cluster),
+        you might do this::
+
+           client = LinodeClient(TOKEN)
+           new_keys_2 = client.object_storage.keys_create(
+               "restricted-keys-2",
+               bucket_access=client.object_storage.bucket_access("us-east-1", "example2", "read_only"),
+           )
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-key-create
+
+        :param label: The label for this keypair, for identification only.
+        :type label: str
+        :param bucket_access: One or a list of dicts with keys "cluster,"
+                              "permissions", and "bucket_name".  If given, the
+                              resulting Object Storage keys will only have the
+                              requested level of access to the requested buckets,
+                              if they exist and are owned by you.  See the provided
+                              :any:`bucket_access` function for a convenient way
+                              to create these dicts.
+        :type bucket_access: dict or list of dict
+
+        :returns: The new keypair, with the secret key populated.
+        :rtype: ObjectStorageKeys
+        """
+        params = {"label": label}
+
+        if bucket_access is not None:
+            if not isinstance(bucket_access, list):
+                bucket_access = [bucket_access]
+
+            ba = [
+                {
+                    "permissions": c.get("permissions"),
+                    "bucket_name": c.get("bucket_name"),
+                    "cluster": c.id
+                    if "cluster" in c and issubclass(type(c["cluster"]), Base)
+                    else c.get("cluster"),
+                }
+                for c in bucket_access
+            ]
+
+            params["bucket_access"] = ba
+
+        result = self.client.post("/object-storage/keys", data=params)
+
+        if not "id" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when creating Object Storage Keys!",
+                json=result,
+            )
+
+        ret = ObjectStorageKeys(self.client, result["id"], result)
+        return ret
+
+    def bucket_access(self, cluster, bucket_name, permissions):
+        return ObjectStorageBucket.access(
+            self, cluster, bucket_name, permissions
+        )
+
+    def cancel(self):
+        """
+        Cancels Object Storage service.  This may be a destructive operation.  Once
+        cancelled, you will no longer receive the transfer for or be billed for
+        Object Storage, and all keys will be invalidated.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-cancel
+        """
+        self.client.post("/object-storage/cancel", data={})
+        return True
+
+    def transfer(self):
+        """
+        The amount of outbound data transfer used by your account’s Object Storage buckets,
+        in bytes, for the current month’s billing cycle. Object Storage adds 1 terabyte
+        of outbound data transfer to your data transfer pool.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-transfer-view
+
+        :returns: The amount of outbound data transfer used by your account’s Object
+                  Storage buckets, in bytes, for the current month’s billing cycle.
+        :rtype: MappedObject
+        """
+        result = self.client.get("/object-storage/transfer")
+
+        if not "used" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when getting Transfer Pool!",
+                json=result,
+            )
+
+        return MappedObject(**result)
+
+    def buckets(self, *filters):
+        """
+        Returns a paginated list of all Object Storage Buckets that you own.
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-buckets-list
+
+        :returns: A list of Object Storage Buckets that matched the query.
+        :rtype: PaginatedList of ObjectStorageBucket
+        """
+        return self.client._get_and_filter(ObjectStorageBucket, *filters)
+
+    def bucket_create(
+        self,
+        cluster,
+        label,
+        acl: ObjectStorageACL = ObjectStorageACL.PRIVATE,
+        cors_enabled=False,
+    ):
+        """
+        Creates an Object Storage Bucket in the specified cluster. Accounts with
+        negative balances cannot access this command. If the bucket already exists
+        and is owned by you, this endpoint returns a 200 response with that bucket
+        as if it had just been created.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-create
+
+        :param acl: The Access Control Level of the bucket using a canned ACL string.
+                    For more fine-grained control of ACLs, use the S3 API directly.
+        :type acl: str
+                   Enum: private,public-read,authenticated-read,public-read-write
+
+        :param cluster: The ID of the Object Storage Cluster where this bucket
+                        should be created.
+        :type cluster: str
+
+        :param cors_enabled: If true, the bucket will be created with CORS enabled for
+                             all origins. For more fine-grained controls of CORS, use
+                             the S3 API directly.
+        :type cors_enabled: bool
+
+        :param label: The name for this bucket. Must be unique in the cluster you are
+                      creating the bucket in, or an error will be returned. Labels will
+                      be reserved only for the cluster that active buckets are created
+                      and stored in. If you want to reserve this bucket’s label in
+                      another cluster, you must create a new bucket with the same label
+                      in the new cluster.
+        :type label: str
+
+        :returns: A Object Storage Buckets that created by user.
+        :rtype: ObjectStorageBucket
+        """
+        cluster_id = (
+            cluster.id if isinstance(cluster, ObjectStorageCluster) else cluster
+        )
+
+        params = {
+            "cluster": cluster_id,
+            "label": label,
+            "acl": acl,
+            "cors_enabled": cors_enabled,
+        }
+
+        result = self.client.post("/object-storage/buckets", data=params)
+
+        if not "label" in result or not "cluster" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when creating Object Storage Bucket!",
+                json=result,
+            )
+
+        return ObjectStorageBucket(
+            self.client, result["label"], result["cluster"], result
+        )
+
+    def object_acl_config(self, cluster_id, bucket, name=None):
+        return ObjectStorageBucket(
+            self.client, bucket, cluster_id
+        ).object_acl_config(name)
+
+    def object_acl_config_update(
+        self, cluster_id, bucket, acl: ObjectStorageACL, name
+    ):
+        return ObjectStorageBucket(
+            self.client, bucket, cluster_id
+        ).object_acl_config_update(acl, name)
+
+    def object_url_create(
+        self,
+        cluster_id,
+        bucket,
+        method,
+        name,
+        content_type=None,
+        expires_in=3600,
+    ):
+        """
+        Creates a pre-signed URL to access a single Object in a bucket.
+        This can be used to share objects, and also to create/delete objects by using
+        the appropriate HTTP method in your request body’s method parameter.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-object-url-create
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param content_type: The expected Content-type header of the request this
+                             signed URL will be valid for. If provided, the
+                             Content-type header must be sent with the request when
+                             this URL is used, and must be the same as it was when
+                             the signed URL was created.
+                             Required for all methods except “GET” or “DELETE”.
+        :type content_type: str
+
+        :param expires_in: How long this signed URL will be valid for, in seconds.
+                           If omitted, the URL will be valid for 3600 seconds (1 hour). Defaults to 3600.
+        :type expires_in: int 360..86400
+
+        :param method: The HTTP method allowed to be used with the pre-signed URL.
+        :type method: str
+
+        :param name: The name of the object that will be accessed with the pre-signed
+                     URL. This object need not exist, and no error will be returned
+                     if it doesn’t. This behavior is useful for generating pre-signed
+                     URLs to upload new objects to by setting the method to “PUT”.
+        :type name: str
+
+        :returns: The signed URL to perform the request at.
+        :rtype: MappedObject
+        """
+        if method not in ("GET", "DELETE") and content_type is None:
+            raise ValueError(
+                "Content-type header is missing for the current method! It's required for all methods except GET or DELETE."
+            )
+        params = {
+            "method": method,
+            "name": name,
+            "expires_in": expires_in,
+            "content_type": content_type,
+        }
+
+        result = self.client.post(
+            "/object-storage/buckets/{}/{}/object-url".format(
+                cluster_id, bucket
+            ),
+            data=drop_null_keys(params),
+        )
+
+        if not "url" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when creating the access url of an object!",
+                json=result,
+            )
+
+        return MappedObject(**result)
diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py
index 8d85515..1357ef8 100644
--- a/linode_api4/linode_client.py
+++ b/linode_api4/linode_client.py
@@ -364,7 +364,7 @@ class LinodeClient:
         )
 
     # helper functions
-    def _get_and_filter(self, obj_type, *filters):
+    def _get_and_filter(self, obj_type, *filters, endpoint=None):
         parsed_filters = None
         if filters:
             if len(filters) > 1:
@@ -374,6 +374,10 @@ class LinodeClient:
             else:
                 parsed_filters = filters[0].dct
 
-        return self._get_objects(
-            obj_type.api_list(), obj_type, filters=parsed_filters
-        )
+        # Use sepcified endpoint
+        if endpoint:
+            return self._get_objects(endpoint, obj_type, filters=parsed_filters)
+        else:
+            return self._get_objects(
+                obj_type.api_list(), obj_type, filters=parsed_filters
+            )
diff --git a/linode_api4/objects/__init__.py b/linode_api4/objects/__init__.py
index 33c2800..256e019 100644
--- a/linode_api4/objects/__init__.py
+++ b/linode_api4/objects/__init__.py
@@ -14,6 +14,6 @@ from .support import *
 from .profile import *
 from .longview import *
 from .tag import Tag
-from .object_storage import ObjectStorageCluster, ObjectStorageKeys
+from .object_storage import *
 from .lke import *
 from .database import *
diff --git a/linode_api4/objects/base.py b/linode_api4/objects/base.py
index 2685908..be72a39 100644
--- a/linode_api4/objects/base.py
+++ b/linode_api4/objects/base.py
@@ -210,9 +210,26 @@ class Base(object, metaclass=FilterableMetaclass):
         if not force and not self._changed:
             return False
 
-        resp = self._client.put(
-            type(self).api_endpoint, model=self, data=self._serialize()
-        )
+        data = None
+        if not self._populated:
+            data = {
+                a: object.__getattribute__(self, a)
+                for a in type(self).properties
+                if type(self).properties[a].mutable
+                and object.__getattribute__(self, a) is not None
+            }
+
+            for key, value in data.items():
+                if (
+                    isinstance(value, ExplicitNullValue)
+                    or value == ExplicitNullValue
+                ):
+                    data[key] = None
+
+        else:
+            data = self._serialize()
+
+        resp = self._client.put(type(self).api_endpoint, model=self, data=data)
 
         if "error" in resp:
             return False
diff --git a/linode_api4/objects/database.py b/linode_api4/objects/database.py
index 4b50fa1..efddee0 100644
--- a/linode_api4/objects/database.py
+++ b/linode_api4/objects/database.py
@@ -39,7 +39,6 @@ class DatabaseEngine(Base):
 
         - MySQL
         - PostgreSQL
-        - MongoDB
 
     API Documentation: https://www.linode.com/docs/api/databases/#managed-database-engine-view
     """
@@ -89,7 +88,6 @@ class DatabaseBackup(DerivedBase):
 
         API Documentation:
 
-            - MongoDB: https://www.linode.com/docs/api/databases/#managed-mongodb-database-backup-restore
             - MySQL: https://www.linode.com/docs/api/databases/#managed-mysql-database-backup-restore
             - PostgreSQL: https://www.linode.com/docs/api/databases/#managed-postgresql-database-backup-restore
         """
@@ -109,16 +107,6 @@ class MySQLDatabaseBackup(DatabaseBackup):
     api_endpoint = "/databases/mysql/instances/{database_id}/backups/{id}"
 
 
-class MongoDBDatabaseBackup(DatabaseBackup):
-    """
-    A backup for an accessible Managed MongoDB Database.
-
-    API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-backup-view
-    """
-
-    api_endpoint = "/databases/mongodb/instances/{database_id}/backups/{id}"
-
-
 class PostgreSQLDatabaseBackup(DatabaseBackup):
     """
     A backup for an accessible Managed PostgreSQL Database.
@@ -397,147 +385,9 @@ class PostgreSQLDatabase(Base):
         Base.invalidate(self)
 
 
-class MongoDBDatabase(Base):
-    """
-    An accessible Managed MongoDB Database.
-
-    API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-view
-    """
-
-    api_endpoint = "/databases/mongodb/instances/{id}"
-
-    properties = {
-        "id": Property(identifier=True),
-        "label": Property(mutable=True),
-        "allow_list": Property(mutable=True),
-        "backups": Property(derived_class=MongoDBDatabaseBackup),
-        "cluster_size": Property(),
-        "compression_type": Property(),
-        "created": Property(is_datetime=True),
-        "encrypted": Property(),
-        "engine": Property(),
-        "hosts": Property(),
-        "peers": Property(),
-        "port": Property(),
-        "region": Property(),
-        "replica_set": Property(),
-        "ssl_connection": Property(),
-        "status": Property(volatile=True),
-        "storage_engine": Property(),
-        "type": Property(),
-        "updated": Property(volatile=True, is_datetime=True),
-        "updates": Property(mutable=True),
-        "version": Property(),
-    }
-
-    @property
-    def credentials(self):
-        """
-        Display the root username and password for an accessible Managed MongoDB Database.
-        The Database must have an active status to perform this command.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-credentials-view
-
-        :returns: MappedObject containing credntials for this DB
-        :rtype: MappedObject
-        """
-
-        if not hasattr(self, "_credentials"):
-            resp = self._client.get(
-                "{}/credentials".format(MongoDBDatabase.api_endpoint),
-                model=self,
-            )
-            self._set("_credentials", MappedObject(**resp))
-
-        return self._credentials
-
-    @property
-    def ssl(self):
-        """
-        Display the SSL CA certificate for an accessible Managed MongoDB Database.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-ssl-certificate-view
-
-        :returns: MappedObject containing SSL CA certificate for this DB
-        :rtype: MappedObject
-        """
-
-        if not hasattr(self, "_ssl"):
-            resp = self._client.get(
-                "{}/ssl".format(MongoDBDatabase.api_endpoint), model=self
-            )
-            self._set("_ssl", MappedObject(**resp))
-
-        return self._ssl
-
-    def credentials_reset(self):
-        """
-        Reset the root password for a Managed MongoDB Database.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-credentials-reset
-
-        :returns: Response from the API call to reset credentials
-        :rtype: dict
-        """
-
-        self.invalidate()
-
-        return self._client.post(
-            "{}/credentials/reset".format(MongoDBDatabase.api_endpoint),
-            model=self,
-        )
-
-    def patch(self):
-        """
-        Apply security patches and updates to the underlying operating system of the Managed MongoDB Database.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-patch
-
-        :returns: Response from the API call to apply security patches
-        :rtype: dict
-        """
-
-        self.invalidate()
-
-        return self._client.post(
-            "{}/patch".format(MongoDBDatabase.api_endpoint), model=self
-        )
-
-    def backup_create(self, label, **kwargs):
-        """
-        Creates a snapshot backup of a Managed MongoDB Database.
-
-        API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-backup-snapshot-create
-        """
-
-        params = {
-            "label": label,
-        }
-        params.update(kwargs)
-
-        self._client.post(
-            "{}/backups".format(MongoDBDatabase.api_endpoint),
-            model=self,
-            data=params,
-        )
-        self.invalidate()
-
-    def invalidate(self):
-        """
-        Clear out cached properties.
-        """
-
-        for attr in ["_ssl", "_credentials"]:
-            if hasattr(self, attr):
-                delattr(self, attr)
-
-        Base.invalidate(self)
-
-
 ENGINE_TYPE_TRANSLATION = {
     "mysql": MySQLDatabase,
     "postgresql": PostgreSQLDatabase,
-    "mongodb": MongoDBDatabase,
 }
 
 
diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py
index 7125866..2a32204 100644
--- a/linode_api4/objects/nodebalancer.py
+++ b/linode_api4/objects/nodebalancer.py
@@ -1,11 +1,23 @@
 import os
 
 from linode_api4.errors import UnexpectedResponseError
-from linode_api4.objects import Base, DerivedBase, Property, Region
+from linode_api4.objects import (
+    Base,
+    DerivedBase,
+    MappedObject,
+    Property,
+    Region,
+)
 from linode_api4.objects.networking import IPAddress
 
 
 class NodeBalancerNode(DerivedBase):
+    """
+    The information about a single Node, a backend for this NodeBalancer’s configured port.
+
+    API documentation: https://www.linode.com/docs/api/nodebalancers/#node-view
+    """
+
     api_endpoint = (
         "/nodebalancers/{nodebalancer_id}/configs/{config_id}/nodes/{id}"
     )
@@ -45,6 +57,12 @@ class NodeBalancerNode(DerivedBase):
 
 
 class NodeBalancerConfig(DerivedBase):
+    """
+    The configuration information for a single port of this NodeBalancer.
+
+    API documentation: https://www.linode.com/docs/api/nodebalancers/#config-view
+    """
+
     api_endpoint = "/nodebalancers/{nodebalancer_id}/configs/{id}"
     derived_url_path = "configs"
     parent_id_name = "nodebalancer_id"
@@ -77,6 +95,14 @@ class NodeBalancerConfig(DerivedBase):
         """
         This is a special derived_class relationship because NodeBalancerNode is the
         only api object that requires two parent_ids
+
+        Returns a paginated list of NodeBalancer nodes associated with this Config.
+        These are the backends that will be sent traffic for this port.
+
+        API documentation: https://www.linode.com/docs/api/nodebalancers/#nodes-list
+
+        :returns: A paginated list of NodeBalancer nodes.
+        :rtype: PaginatedList of NodeBalancerNode
         """
         if not hasattr(self, "_nodes"):
             base_url = "{}/{}".format(
@@ -95,6 +121,24 @@ class NodeBalancerConfig(DerivedBase):
         return self._nodes
 
     def node_create(self, label, address, **kwargs):
+        """
+        Creates a NodeBalancer Node, a backend that can accept traffic for this
+        NodeBalancer Config. Nodes are routed requests on the configured port based
+        on their status.
+
+        API documentation: https://www.linode.com/docs/api/nodebalancers/#node-create
+
+        :param address: The private IP Address where this backend can be reached.
+                        This must be a private IP address.
+        :type address: str
+
+        :param label: The label for this node. This is for display purposes only.
+                      Must have a length between 2 and 32 characters.
+        :type label: str
+
+        :returns: The node which is created successfully.
+        :rtype: NodeBalancerNode
+        """
         params = {
             "label": label,
             "address": address,
@@ -152,6 +196,12 @@ class NodeBalancerConfig(DerivedBase):
 
 
 class NodeBalancer(Base):
+    """
+    A single NodeBalancer you can access.
+
+    API documentation: https://www.linode.com/docs/api/nodebalancers/#nodebalancer-view
+    """
+
     api_endpoint = "/nodebalancers/{id}"
     properties = {
         "id": Property(identifier=True),
@@ -165,13 +215,23 @@ class NodeBalancer(Base):
         "ipv6": Property(),
         "region": Property(slug_relationship=Region),
         "configs": Property(derived_class=NodeBalancerConfig),
+        "transfer": Property(),
+        "tags": Property(),
     }
 
     # create derived objects
-    def config_create(self, label=None, **kwargs):
+    def config_create(self, **kwargs):
+        """
+        Creates a NodeBalancer Config, which allows the NodeBalancer to accept traffic
+        on a new port. You will need to add NodeBalancer Nodes to the new Config before
+        it can actually serve requests.
+
+        API documentation: https://www.linode.com/docs/api/nodebalancers/#config-create
+
+        :returns: The config that created successfully.
+        :rtype: NodeBalancerConfig
+        """
         params = kwargs
-        if label:
-            params["label"] = label
 
         result = self._client.post(
             "{}/configs".format(NodeBalancer.api_endpoint),
@@ -187,3 +247,58 @@ class NodeBalancer(Base):
 
         c = NodeBalancerConfig(self._client, result["id"], self.id, result)
         return c
+
+    def config_rebuild(self, config_id, nodes, **kwargs):
+        """
+        Rebuilds a NodeBalancer Config and its Nodes that you have permission to modify.
+        Use this command to update a NodeBalancer’s Config and Nodes with a single request.
+
+        API documentation: https://www.linode.com/docs/api/nodebalancers/#config-rebuild
+
+        :param config_id: The ID of the Config to access.
+        :type config_id: int
+
+        :param nodes: The NodeBalancer Node(s) that serve this Config.
+        :type nodes: [{ address: str, id: int, label: str, mode: str, weight: int }]
+
+        :returns: A nodebalancer config that rebuilt successfully.
+        :rtype: NodeBalancerConfig
+        """
+        params = {
+            "nodes": nodes,
+        }
+        params.update(kwargs)
+
+        result = self._client.post(
+            "{}/configs/{}/rebuild".format(
+                NodeBalancer.api_endpoint, config_id
+            ),
+            model=self,
+            data=params,
+        )
+
+        if not "id" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response rebuilding config!", json=result
+            )
+
+        return NodeBalancerConfig(self._client, result["id"], self.id, result)
+
+    def statistics(self):
+        """
+        Returns detailed statistics about the requested NodeBalancer.
+
+        API documentation: https://www.linode.com/docs/api/nodebalancers/#nodebalancer-statistics-view
+
+        :returns: The requested stats.
+        :rtype: MappedObject
+        """
+        result = self._client.get(
+            "{}/stats".format(NodeBalancer.api_endpoint), model=self
+        )
+
+        if not "title" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response generating stats!", json=result
+            )
+        return MappedObject(**result)
diff --git a/linode_api4/objects/object_storage.py b/linode_api4/objects/object_storage.py
index b2be22d..d96256c 100644
--- a/linode_api4/objects/object_storage.py
+++ b/linode_api4/objects/object_storage.py
@@ -1,9 +1,465 @@
-from linode_api4.objects import Base, DerivedBase, Image, Property, Region
+from linode_api4.errors import UnexpectedResponseError
+from linode_api4.objects import (
+    Base,
+    DerivedBase,
+    MappedObject,
+    Property,
+    Region,
+)
+from linode_api4.util import drop_null_keys
+
+
+class ObjectStorageACL:
+    PRIVATE = "private"
+    PUBLIC_READ = "public-read"
+    AUTHENTICATED_READ = "authenticated-read"
+    PUBLIC_READ_WRITE = "public-read-write"
+    CUSTOM = "custom"
+
+
+class ObjectStorageBucket(DerivedBase):
+    """
+    A bucket where objects are stored in.
+
+    API documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-view
+    """
+
+    api_endpoint = "/object-storage/buckets/{cluster}/{label}"
+    parent_id_name = "cluster"
+    id_attribute = "label"
+
+    properties = {
+        "cluster": Property(),
+        "created": Property(is_datetime=True),
+        "hostname": Property(),
+        "label": Property(),
+        "objects": Property(),
+        "size": Property(),
+    }
+
+    @classmethod
+    def api_list(cls):
+        """
+        Override this method to return the correct URL that will produce
+        a list of JSON objects of this class' type - Object Storage Bucket.
+        """
+        return "/".join(cls.api_endpoint.split("/")[:-2])
+
+    @classmethod
+    def make_instance(cls, id, client, parent_id=None, json=None):
+        """
+        Override this method to pass in the parent_id from the _raw_json object
+        when it's available.
+        """
+        if json is None:
+            return None
+        if parent_id is None and json["cluster"]:
+            parent_id = json["cluster"]
+
+        if parent_id:
+            return super().make(id, client, cls, parent_id=parent_id, json=json)
+        else:
+            raise UnexpectedResponseError(
+                "Unexpected json response when making a new Object Storage Bucket instance."
+            )
+
+    def access_modify(
+        self,
+        acl: ObjectStorageACL = None,
+        cors_enabled=None,
+    ):
+        """
+        Allows changing basic Cross-origin Resource Sharing (CORS) and Access Control
+        Level (ACL) settings. Only allows enabling/disabling CORS for all origins,
+        and/or setting canned ACLs. For more fine-grained control of both systems,
+        please use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-access-modify
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param acl: The Access Control Level of the bucket using a canned ACL string.
+                    For more fine-grained control of ACLs, use the S3 API directly.
+        :type acl: str
+                   Enum: private,public-read,authenticated-read,public-read-write
+
+        :param cors_enabled: If true, the bucket will be created with CORS enabled for
+                             all origins. For more fine-grained controls of CORS, use
+                             the S3 API directly.
+        :type cors_enabled: bool
+        """
+        params = {
+            "acl": acl,
+            "cors_enabled": cors_enabled,
+        }
+
+        resp = self._client.post(
+            "/object-storage/buckets/{}/{}/access".format(
+                self.cluster, self.id
+            ),
+            data=drop_null_keys(params),
+        )
+
+        if "errors" in resp:
+            raise UnexpectedResponseError(
+                "Unexpected response when modifying the access to a bucket!",
+                json=resp,
+            )
+        return True
+
+    def access_update(
+        self,
+        acl: ObjectStorageACL = None,
+        cors_enabled=None,
+    ):
+        """
+        Allows changing basic Cross-origin Resource Sharing (CORS) and Access Control
+        Level (ACL) settings. Only allows enabling/disabling CORS for all origins,
+        and/or setting canned ACLs. For more fine-grained control of both systems,
+        please use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-access-update
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param acl: The Access Control Level of the bucket using a canned ACL string.
+                    For more fine-grained control of ACLs, use the S3 API directly.
+        :type acl: str
+                   Enum: private,public-read,authenticated-read,public-read-write
+
+        :param cors_enabled: If true, the bucket will be created with CORS enabled for
+                             all origins. For more fine-grained controls of CORS,
+                             use the S3 API directly.
+        :type cors_enabled: bool
+        """
+        params = {
+            "acl": acl,
+            "cors_enabled": cors_enabled,
+        }
+
+        resp = self._client.put(
+            "/object-storage/buckets/{}/{}/access".format(
+                self.cluster, self.id
+            ),
+            data=drop_null_keys(params),
+        )
+
+        if "errors" in resp:
+            raise UnexpectedResponseError(
+                "Unexpected response when updating the access to a bucket!",
+                json=resp,
+            )
+        return True
+
+    def ssl_cert_delete(self):
+        """
+        Deletes this Object Storage bucket’s user uploaded TLS/SSL certificate
+        and private key.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-tlsssl-cert-delete
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :returns: True if the TLS/SSL certificate and private key in the bucket were successfully deleted.
+        :rtype: bool
+        """
+
+        resp = self._client.delete(
+            "/object-storage/buckets/{}/{}/ssl".format(self.cluster, self.id)
+        )
+
+        if "error" in resp:
+            raise UnexpectedResponseError(
+                "Unexpected response when deleting a bucket!",
+                json=resp,
+            )
+        return True
+
+    def ssl_cert(self):
+        """
+        Returns a result object which wraps a bool value indicating
+        if this bucket has a corresponding TLS/SSL certificate that
+        was uploaded by an Account user.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-tlsssl-cert-view
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :returns: A result object which has a bool field indicating if this Bucket has a corresponding
+                  TLS/SSL certificate that was uploaded by an Account user.
+        :rtype: MappedObject
+        """
+        result = self._client.get(
+            "/object-storage/buckets/{}/{}/ssl".format(self.cluster, self.id)
+        )
+
+        if not "ssl" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when getting the TLS/SSL certs indicator of a bucket!",
+                json=result,
+            )
+
+        return MappedObject(**result)
+
+    def ssl_cert_upload(self, certificate, private_key):
+        """
+        Upload a TLS/SSL certificate and private key to be served when you
+        visit your Object Storage bucket via HTTPS. Your TLS/SSL certificate and
+        private key are stored encrypted at rest.
+
+        To replace an expired certificate, delete your current certificate and
+        upload a new one.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-tlsssl-cert-upload
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param certificate: Your Base64 encoded and PEM formatted SSL certificate.
+                            Line breaks must be represented as “\n” in the string
+                            for requests (but not when using the Linode CLI)
+        :type certificate: str
+
+        :param private_key: The private key associated with this TLS/SSL certificate.
+                            Line breaks must be represented as “\n” in the string
+                            for requests (but not when using the Linode CLI)
+        :type private_key: str
+
+        :returns: A result object which has a bool field indicating if this Bucket has a corresponding
+                  TLS/SSL certificate that was uploaded by an Account user.
+        :rtype: MappedObject
+        """
+        params = {
+            "certificate": certificate,
+            "private_key": private_key,
+        }
+        result = self._client.post(
+            "/object-storage/buckets/{}/{}/ssl".format(self.cluster, self.id),
+            data=params,
+        )
+
+        if not "ssl" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when uploading TLS/SSL certs!",
+                json=result,
+            )
+
+        return MappedObject(**result)
+
+    def contents(
+        self,
+        marker=None,
+        delimiter=None,
+        prefix=None,
+        page_size=100,
+    ):
+        """
+        Returns the contents of a bucket.
+        The contents are paginated using a marker, which is the name of the last object
+        on the previous page. Objects may be filtered by prefix and delimiter as well;
+        see Query Parameters for more information.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-contents-list
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param marker: The “marker” for this request, which can be used to paginate
+                       through large buckets. Its value should be the value of the
+                       next_marker property returned with the last page. Listing
+                       bucket contents does not support arbitrary page access. See the
+                       next_marker property in the responses section for more details.
+        :type marker: str
+
+        :param delimiter: The delimiter for object names; if given, object names will
+                          be returned up to the first occurrence of this character.
+                          This is most commonly used with the / character to allow
+                          bucket transversal in a manner similar to a filesystem,
+                          however any delimiter may be used. Use in conjunction with
+                          prefix to see object names past the first occurrence of
+                          the delimiter.
+        :type delimiter: str
+
+        :param prefix: Filters objects returned to only those whose name start with
+                       the given prefix. Commonly used in conjunction with delimiter
+                       to allow transversal of bucket contents in a manner similar to
+                       a filesystem.
+        :type perfix: str
+
+        :param page_size: The number of items to return per page. Defaults to 100.
+        :type page_size: int 25..500
+
+        :returns: A list of the MappedObject of the requested bucket's contents.
+        :rtype: [MappedObject]
+        """
+        params = {
+            "marker": marker,
+            "delimiter": delimiter,
+            "prefix": prefix,
+            "page_size": page_size,
+        }
+        result = self._client.get(
+            "/object-storage/buckets/{}/{}/object-list".format(
+                self.cluster, self.id
+            ),
+            data=drop_null_keys(params),
+        )
+
+        if not "data" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when getting the contents of a bucket!",
+                json=result,
+            )
+
+        return [MappedObject(**c) for c in result["data"]]
+
+    def object_acl_config(self, name=None):
+        """
+        View an Object’s configured Access Control List (ACL) in this Object Storage
+        bucket. ACLs define who can access your buckets and objects and specify the
+        level of access granted to those users.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-object-acl-config-view
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param name: The name of the object for which to retrieve its Access Control
+                     List (ACL). Use the Object Storage Bucket Contents List endpoint
+                     to access all object names in a bucket.
+        :type name: str
+
+        :returns: The Object's canned ACL and policy.
+        :rtype: MappedObject
+        """
+        params = {
+            "name": name,
+        }
+
+        result = self._client.get(
+            f"{ObjectStorageBucket.api_endpoint}/object-acl",
+            model=self,
+            data=drop_null_keys(params),
+        )
+
+        if not "acl" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when viewing Object’s configured ACL!",
+                json=result,
+            )
+
+        return MappedObject(**result)
+
+    def object_acl_config_update(self, acl: ObjectStorageACL, name):
+        """
+        Update an Object’s configured Access Control List (ACL) in this Object Storage
+        bucket. ACLs define who can access your buckets and objects and specify the
+        level of access granted to those users.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-object-acl-config-update
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :param bucket: The bucket name.
+        :type bucket: str
+
+        :param acl: The Access Control Level of the bucket, as a canned ACL string.
+                    For more fine-grained control of ACLs, use the S3 API directly.
+        :type acl: str
+                   Enum: private,public-read,authenticated-read,public-read-write,custom
+
+        :param name: The name of the object for which to retrieve its Access Control
+                     List (ACL). Use the Object Storage Bucket Contents List endpoint
+                     to access all object names in a bucket.
+        :type name: str
+
+        :returns: The Object's canned ACL and policy.
+        :rtype: MappedObject
+        """
+        params = {
+            "acl": acl,
+            "name": name,
+        }
+
+        result = self._client.put(
+            f"{ObjectStorageBucket.api_endpoint}/object-acl",
+            model=self,
+            data=params,
+        )
+
+        if not "acl" in result:
+            raise UnexpectedResponseError(
+                "Unexpected response when updating Object’s configured ACL!",
+                json=result,
+            )
+
+        return MappedObject(**result)
+
+    def access(self, cluster, bucket_name, permissions):
+        """
+        Returns a dict formatted to be included in the `bucket_access` argument
+        of :any:`keys_create`.  See the docs for that method for an example of
+        usage.
+
+        :param cluster: The Object Storage cluster to grant access in.
+        :type cluster: :any:`ObjectStorageCluster` or str
+        :param bucket_name: The name of the bucket to grant access to.
+        :type bucket_name: str
+        :param permissions: The permissions to grant.  Should be one of "read_only"
+                            or "read_write".
+        :type permissions: str
+
+        :returns: A dict formatted correctly for specifying bucket access for
+                  new keys.
+        :rtype: dict
+        """
+        return {
+            "cluster": cluster,
+            "bucket_name": bucket_name,
+            "permissions": permissions,
+        }
 
 
 class ObjectStorageCluster(Base):
     """
     A cluster where Object Storage is available.
+
+    API documentation: https://www.linode.com/docs/api/object-storage/#cluster-view
     """
 
     api_endpoint = "/object-storage/clusters/{id}"
@@ -16,10 +472,34 @@ class ObjectStorageCluster(Base):
         "static_site_domain": Property(),
     }
 
+    def buckets_in_cluster(self, *filters):
+        """
+        Returns a list of Buckets in this cluster belonging to this Account.
+
+        This endpoint is available for convenience.
+        It is recommended that instead you use the more fully-featured S3 API directly.
+
+        API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-buckets-in-cluster-list
+
+        :param cluster_id: The ID of the cluster this bucket exists in.
+        :type cluster_id: str
+
+        :returns: A list of Object Storage Buckets that in the requested cluster.
+        :rtype: PaginatedList of ObjectStorageBucket
+        """
+
+        return self._client._get_and_filter(
+            ObjectStorageBucket,
+            *filters,
+            endpoint="/object-storage/buckets/{}".format(self.id),
+        )
+
 
 class ObjectStorageKeys(Base):
     """
     A keypair that allows third-party applications to access Linode Object Storage.
+
+    API documentation: https://www.linode.com/docs/api/object-storage/#object-storage-key-view
     """
 
     api_endpoint = "/object-storage/keys/{id}"
@@ -29,4 +509,6 @@ class ObjectStorageKeys(Base):
         "label": Property(mutable=True),
         "access_key": Property(),
         "secret_key": Property(),
+        "bucket_access": Property(),
+        "limited": Property(),
     }
diff --git a/linode_api4/objects/support.py b/linode_api4/objects/support.py
index b9a817f..78d1d86 100644
--- a/linode_api4/objects/support.py
+++ b/linode_api4/objects/support.py
@@ -1,3 +1,6 @@
+from pathlib import Path
+from typing import Union
+
 import requests
 
 from linode_api4.errors import ApiError, UnexpectedResponseError
@@ -139,7 +142,7 @@ class SupportTicket(Base):
         r = TicketReply(self._client, result["id"], self.id, result)
         return r
 
-    def upload_attachment(self, attachment):
+    def upload_attachment(self, attachment: Union[Path, str]):
         """
         Uploads an attachment to an existing Support Ticket.
 
@@ -151,27 +154,25 @@ class SupportTicket(Base):
         :returns: Whether the upload operation was successful.
         :rtype: bool
         """
+        if not isinstance(attachment, Path):
+            attachment = Path(attachment)
 
-        content = None
-        with open(attachment) as f:
-            content = f.read()
-
-        if not content:
-            raise ValueError("Nothing to upload!")
+        if not attachment.exists():
+            raise ValueError("File not exist, nothing to upload.")
 
         headers = {
-            "Authorization": "token {}".format(self._client.token),
-            "Content-type": "multipart/form-data",
+            "Authorization": "Bearer {}".format(self._client.token),
         }
 
-        result = requests.post(
-            "{}{}/attachments".format(
-                self._client.base_url,
-                SupportTicket.api_endpoint.format(id=self.id),
-            ),
-            headers=headers,
-            files=content,
-        )
+        with open(attachment, "rb") as f:
+            result = requests.post(
+                "{}{}/attachments".format(
+                    self._client.base_url,
+                    SupportTicket.api_endpoint.format(id=self.id),
+                ),
+                headers=headers,
+                files={"file": f},
+            )
 
         if not result.status_code == 200:
             errors = []
diff --git a/linode_api4/objects/volume.py b/linode_api4/objects/volume.py
index 1bd6811..c406191 100644
--- a/linode_api4/objects/volume.py
+++ b/linode_api4/objects/volume.py
@@ -3,6 +3,13 @@ from linode_api4.objects import Base, Instance, Property, Region
 
 
 class Volume(Base):
+    """
+    A single Block Storage Volume. Block Storage Volumes are persistent storage devices
+    that can be attached to a Compute Instance and used to store any type of data.
+
+    API Documentation: https://www.linode.com/docs/api/volumes/#volume-view
+    """
+
     api_endpoint = "/volumes/{id}"
 
     properties = {
@@ -22,7 +29,17 @@ class Volume(Base):
 
     def attach(self, to_linode, config=None):
         """
-        Attaches this Volume to the given Linode
+        Attaches this Volume to the given Linode.
+
+        API Documentation: https://www.linode.com/docs/api/volumes/#volume-attach
+
+        :param to_linode: The ID or object of the Linode to attach the volume to.
+        :type to_linode: Union[Instance, int]
+
+        :param config: The ID or object of the Linode Config to include this Volume in.
+                       Must belong to the Linode referenced by linode_id.
+                       If not given, the last booted Config will be chosen.
+        :type config: Union[Config, int]
         """
         result = self._client.post(
             "{}/attach".format(Volume.api_endpoint),
@@ -50,6 +67,11 @@ class Volume(Base):
     def detach(self):
         """
         Detaches this Volume if it is attached
+
+        API Documentation: https://www.linode.com/docs/api/volumes/#volume-detach
+
+        :returns: Returns true if operation was successful
+        :rtype: bool
         """
         self._client.post("{}/detach".format(Volume.api_endpoint), model=self)
 
@@ -58,6 +80,14 @@ class Volume(Base):
     def resize(self, size):
         """
         Resizes this Volume
+
+        API Documentation: https://www.linode.com/docs/api/volumes/#volume-resize
+
+        :param size: The Volume’s size, in GiB.
+        :type size: int
+
+        :returns: Returns true if operation was successful
+        :rtype: bool
         """
         result = self._client.post(
             "{}/resize".format(Volume.api_endpoint),
@@ -73,9 +103,13 @@ class Volume(Base):
         """
         Clones this volume to a new volume in the same region with the given label
 
+        API Documentation: https://www.linode.com/docs/api/volumes/#volume-clone
+
         :param label: The label for the new volume.
+        :type label: str
 
         :returns: The new volume object.
+        :rtype: Volume
         """
         result = self._client.post(
             "{}/clone".format(Volume.api_endpoint),


Test patch:
diff --git a/test/fixtures.py b/test/fixtures.py
index 94f3cee..a4609b2 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -41,5 +41,5 @@ class TestFixtures:
             if "results" in data:
                 # this is a paginated response
                 for obj in data["data"]:
-                    if "id" in obj:  # tags don't have ids
+                    if "id" in obj:  # tags, obj-buckets don't have ids
                         self.fixtures[fixture_url + "/" + str(obj["id"])] = obj
diff --git a/test/fixtures/databases_mongodb_instances.json b/test/fixtures/databases_mongodb_instances.json
deleted file mode 100644
index 383a1b2..0000000
--- a/test/fixtures/databases_mongodb_instances.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  "data": [
-    {
-      "allow_list": [
-        "203.0.113.1/32",
-        "192.0.1.0/24"
-      ],
-      "cluster_size": 3,
-      "compression_type": "none",
-      "created": "2022-01-01T00:01:01",
-      "encrypted": false,
-      "engine": "mongodb",
-      "hosts": {
-        "primary": "lin-0000-0000.servers.linodedb.net",
-        "secondary": null
-      },
-      "id": 123,
-      "label": "example-db",
-      "peers": [
-        "lin-0000-0000.servers.linodedb.net",
-        "lin-0000-0001.servers.linodedb.net",
-        "lin-0000-0002.servers.linodedb.net"
-      ],
-      "port": 27017,
-      "region": "us-east",
-      "replica_set": null,
-      "ssl_connection": true,
-      "status": "active",
-      "storage_engine": "wiredtiger",
-      "type": "g6-dedicated-2",
-      "updated": "2022-01-01T00:01:01",
-      "updates": {
-        "day_of_week": 1,
-        "duration": 3,
-        "frequency": "weekly",
-        "hour_of_day": 0,
-        "week_of_month": null
-      },
-      "version": "4.4.10"
-    }
-  ],
-  "page": 1,
-  "pages": 1,
-  "results": 1
-}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_backups.json b/test/fixtures/databases_mongodb_instances_123_backups.json
deleted file mode 100644
index 671c688..0000000
--- a/test/fixtures/databases_mongodb_instances_123_backups.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "data": [
-    {
-      "created": "2022-01-01T00:01:01",
-      "id": 456,
-      "label": "Scheduled - 02/04/22 11:11 UTC-XcCRmI",
-      "type": "auto"
-    }
-  ],
-  "page": 1,
-  "pages": 1,
-  "results": 1
-}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_backups_456_restore.json b/test/fixtures/databases_mongodb_instances_123_backups_456_restore.json
deleted file mode 100644
index 9e26dfe..0000000
--- a/test/fixtures/databases_mongodb_instances_123_backups_456_restore.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_credentials.json b/test/fixtures/databases_mongodb_instances_123_credentials.json
deleted file mode 100644
index 217c27c..0000000
--- a/test/fixtures/databases_mongodb_instances_123_credentials.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "password": "s3cur3P@ssw0rd",
-  "username": "linroot"
-}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_credentials_reset.json b/test/fixtures/databases_mongodb_instances_123_credentials_reset.json
deleted file mode 100644
index 9e26dfe..0000000
--- a/test/fixtures/databases_mongodb_instances_123_credentials_reset.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_patch.json b/test/fixtures/databases_mongodb_instances_123_patch.json
deleted file mode 100644
index 9e26dfe..0000000
--- a/test/fixtures/databases_mongodb_instances_123_patch.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{}
\ No newline at end of file
diff --git a/test/fixtures/databases_mongodb_instances_123_ssl.json b/test/fixtures/databases_mongodb_instances_123_ssl.json
deleted file mode 100644
index a331c5c..0000000
--- a/test/fixtures/databases_mongodb_instances_123_ssl.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "ca_certificate": "LS0tLS1CRUdJ...=="
-}
\ No newline at end of file
diff --git a/test/fixtures/databases_types.json b/test/fixtures/databases_types.json
index fec5c02..d852327 100644
--- a/test/fixtures/databases_types.json
+++ b/test/fixtures/databases_types.json
@@ -5,15 +5,6 @@
       "deprecated": false,
       "disk": 25600,
       "engines": {
-        "mongodb": [
-          {
-            "price": {
-              "hourly": 0.03,
-              "monthly": 20
-            },
-            "quantity": 1
-          }
-        ],
         "mysql": [
           {
             "price": {
diff --git a/test/fixtures/nodebalancers_12345_configs_4567_rebuild.json b/test/fixtures/nodebalancers_12345_configs_4567_rebuild.json
new file mode 100644
index 0000000..d4b6f0c
--- /dev/null
+++ b/test/fixtures/nodebalancers_12345_configs_4567_rebuild.json
@@ -0,0 +1,25 @@
+{
+    "algorithm": "roundrobin",
+    "check": "http_body",
+    "check_attempts": 3,
+    "check_body": "it works",
+    "check_interval": 90,
+    "check_passive": true,
+    "check_path": "/test",
+    "check_timeout": 10,
+    "cipher_suite": "recommended",
+    "id": 4567,
+    "nodebalancer_id": 12345,
+    "nodes_status": {
+      "down": 0,
+      "up": 4
+    },
+    "port": 80,
+    "protocol": "http",
+    "proxy_protocol": "none",
+    "ssl_cert": "<REDACTED>",
+    "ssl_commonname": "www.example.com",
+    "ssl_fingerprint": "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13",
+    "ssl_key": "<REDACTED>",
+    "stickiness": "http_cookie"
+}
\ No newline at end of file
diff --git a/test/fixtures/nodebalancers_12345_stats.json b/test/fixtures/nodebalancers_12345_stats.json
new file mode 100644
index 0000000..5e18246
--- /dev/null
+++ b/test/fixtures/nodebalancers_12345_stats.json
@@ -0,0 +1,16 @@
+{
+    "data": {
+      "connections": [
+        null
+      ],
+      "traffic": {
+        "in": [
+          null
+        ],
+        "out": [
+          null
+        ]
+      }
+    },
+    "title": "linode.com - balancer12345 (12345) - day (5 min avg)"
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets.json b/test/fixtures/object-storage_buckets.json
new file mode 100644
index 0000000..f99a944
--- /dev/null
+++ b/test/fixtures/object-storage_buckets.json
@@ -0,0 +1,15 @@
+{
+    "data": [
+      {
+        "cluster": "us-east-1",
+        "created": "2019-01-01T01:23:45",
+        "hostname": "example-bucket.us-east-1.linodeobjects.com",
+        "label": "example-bucket",
+        "objects": 4,
+        "size": 188318981
+      }
+    ],
+    "page": 1,
+    "pages": 1,
+    "results": 1
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1.json b/test/fixtures/object-storage_buckets_us-east-1.json
new file mode 100644
index 0000000..f99a944
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1.json
@@ -0,0 +1,15 @@
+{
+    "data": [
+      {
+        "cluster": "us-east-1",
+        "created": "2019-01-01T01:23:45",
+        "hostname": "example-bucket.us-east-1.linodeobjects.com",
+        "label": "example-bucket",
+        "objects": 4,
+        "size": 188318981
+      }
+    ],
+    "page": 1,
+    "pages": 1,
+    "results": 1
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json
new file mode 100644
index 0000000..b8c9450
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json
@@ -0,0 +1,8 @@
+{
+  "cluster": "us-east-1",
+  "created": "2019-01-01T01:23:45",
+  "hostname": "example-bucket.us-east-1.linodeobjects.com",
+  "label": "example-bucket",
+  "objects": 4,
+  "size": 188318981
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-acl.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-acl.json
new file mode 100644
index 0000000..a9b9aaf
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-acl.json
@@ -0,0 +1,4 @@
+{
+    "acl": "public-read",
+    "acl_xml": "<AccessControlPolicy>...</AccessControlPolicy>"
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-list.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-list.json
new file mode 100644
index 0000000..6d92be5
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-list.json
@@ -0,0 +1,13 @@
+{
+    "data": [
+      {
+        "etag": "9f254c71e28e033bf9e0e5262e3e72ab",
+        "is_truncated": true,
+        "last_modified": "2019-01-01T01:23:45",
+        "name": "example",
+        "next_marker": "bd021c21-e734-4823-97a4-58b41c2cd4c8.892602.184",
+        "owner": "bfc70ab2-e3d4-42a4-ad55-83921822270c",
+        "size": 123
+      }
+    ]
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-url.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-url.json
new file mode 100644
index 0000000..de61777
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_object-url.json
@@ -0,0 +1,3 @@
+{
+    "url": "https://us-east-1.linodeobjects.com/example-bucket/example?Signature=qr98TEucCntPgEG%2BsZQGDsJg93c%3D&Expires=1567609905&AWSAccessKeyId=G4YAF81XWY61DQM94SE0"
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket_ssl.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_ssl.json
new file mode 100644
index 0000000..e16ebc3
--- /dev/null
+++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket_ssl.json
@@ -0,0 +1,3 @@
+{
+    "ssl": true
+}
\ No newline at end of file
diff --git a/test/fixtures/object-storage_transfer.json b/test/fixtures/object-storage_transfer.json
new file mode 100644
index 0000000..ae1968e
--- /dev/null
+++ b/test/fixtures/object-storage_transfer.json
@@ -0,0 +1,3 @@
+{
+    "used": 12956600198
+}
\ No newline at end of file
diff --git a/test/linode_client_test.py b/test/linode_client_test.py
index d41d9fc..35378d4 100644
--- a/test/linode_client_test.py
+++ b/test/linode_client_test.py
@@ -6,6 +6,10 @@ from unittest.mock import MagicMock
 from linode_api4 import ApiError, LinodeClient, LongviewSubscription
 from linode_api4.objects.linode import Instance
 from linode_api4.objects.networking import IPAddress
+from linode_api4.objects.object_storage import (
+    ObjectStorageACL,
+    ObjectStorageCluster,
+)
 
 
 class LinodeClientGeneralTest(ClientBaseCase):
@@ -840,6 +844,115 @@ class ObjectStorageGroupTest(ClientBaseCase):
             self.assertEqual(m.call_url, "/object-storage/keys")
             self.assertEqual(m.call_data, {"label": "object-storage-key-1"})
 
+    def test_transfer(self):
+        """
+        Test that you can get the amount of outbound data transfer
+        used by your account’s Object Storage buckets
+        """
+        object_storage_transfer_url = "/object-storage/transfer"
+
+        with self.mock_get(object_storage_transfer_url) as m:
+            result = self.client.object_storage.transfer()
+            self.assertEqual(result.used, 12956600198)
+            self.assertEqual(m.call_url, object_storage_transfer_url)
+
+    def test_buckets(self):
+        """
+        Test that Object Storage Buckets can be reterived
+        """
+        object_storage_buckets_url = "/object-storage/buckets"
+
+        with self.mock_get(object_storage_buckets_url) as m:
+            buckets = self.client.object_storage.buckets()
+            self.assertIsNotNone(buckets)
+            bucket = buckets[0]
+
+            self.assertEqual(m.call_url, object_storage_buckets_url)
+            self.assertEqual(bucket.cluster, "us-east-1")
+            self.assertEqual(
+                bucket.created,
+                datetime(
+                    year=2019, month=1, day=1, hour=1, minute=23, second=45
+                ),
+            )
+            self.assertEqual(
+                bucket.hostname, "example-bucket.us-east-1.linodeobjects.com"
+            )
+            self.assertEqual(bucket.label, "example-bucket")
+            self.assertEqual(bucket.objects, 4)
+            self.assertEqual(bucket.size, 188318981)
+
+    def test_bucket_create(self):
+        """
+        Test that you can create a Object Storage Bucket
+        """
+        # buckets don't work like a normal RESTful collection, so we have to do this
+        with self.mock_post(
+            {"label": "example-bucket", "cluster": "us-east-1"}
+        ) as m:
+            b = self.client.object_storage.bucket_create(
+                "us-east-1", "example-bucket", ObjectStorageACL.PRIVATE, True
+            )
+            self.assertIsNotNone(b)
+            self.assertEqual(m.call_url, "/object-storage/buckets")
+            self.assertEqual(
+                m.call_data,
+                {
+                    "label": "example-bucket",
+                    "cluster": "us-east-1",
+                    "cors_enabled": True,
+                    "acl": "private",
+                },
+            )
+
+        """
+        Test that you can create a Object Storage Bucket passing a Cluster object
+        """
+        with self.mock_post(
+            {"label": "example-bucket", "cluster": "us-east-1"}
+        ) as m:
+            cluster = ObjectStorageCluster(self.client, "us-east-1")
+            b = self.client.object_storage.bucket_create(
+                cluster, "example-bucket", "private", True
+            )
+            self.assertIsNotNone(b)
+            self.assertEqual(m.call_url, "/object-storage/buckets")
+            self.assertEqual(
+                m.call_data,
+                {
+                    "label": "example-bucket",
+                    "cluster": "us-east-1",
+                    "cors_enabled": True,
+                    "acl": "private",
+                },
+            )
+
+    def test_object_url_create(self):
+        """
+        Test that you can create pre-signed URL to access a single Object in a bucket.
+        """
+        object_url_create_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/object-url"
+        )
+        with self.mock_post(object_url_create_url) as m:
+            result = self.client.object_storage.object_url_create(
+                "us-east-1", "example-bucket", "GET", "example"
+            )
+            self.assertIsNotNone(result)
+            self.assertEqual(m.call_url, object_url_create_url)
+            self.assertEqual(
+                result.url,
+                "https://us-east-1.linodeobjects.com/example-bucket/example?Signature=qr98TEucCntPgEG%2BsZQGDsJg93c%3D&Expires=1567609905&AWSAccessKeyId=G4YAF81XWY61DQM94SE0",
+            )
+            self.assertEqual(
+                m.call_data,
+                {
+                    "method": "GET",
+                    "name": "example",
+                    "expires_in": 3600,
+                },
+            )
+
 
 class NetworkingGroupTest(ClientBaseCase):
     """
diff --git a/test/objects/database_test.py b/test/objects/database_test.py
index a70f3ae..e32368d 100644
--- a/test/objects/database_test.py
+++ b/test/objects/database_test.py
@@ -1,6 +1,6 @@
 from test.base import ClientBaseCase
 
-from linode_api4 import MongoDBDatabase, PostgreSQLDatabase
+from linode_api4 import PostgreSQLDatabase
 from linode_api4.objects import MySQLDatabase
 
 
@@ -18,7 +18,7 @@ class DatabaseTest(ClientBaseCase):
         self.assertEqual(len(types), 1)
         self.assertEqual(types[0].type_class, "nanode")
         self.assertEqual(types[0].id, "g6-nanode-1")
-        self.assertEqual(types[0].engines.mongodb[0].price.monthly, 20)
+        self.assertEqual(types[0].engines.mysql[0].price.monthly, 20)
 
     def test_get_engines(self):
         """
@@ -440,187 +440,3 @@ class PostgreSQLDatabaseTest(ClientBaseCase):
                 m.call_url,
                 "/databases/postgresql/instances/123/credentials/reset",
             )
-
-
-class MongoDBDatabaseTest(ClientBaseCase):
-    """
-    Tests methods of the MongoDBDatabase class
-    """
-
-    def test_get_instances(self):
-        """
-        Test that database types are properly handled
-        """
-        dbs = self.client.database.mongodb_instances()
-
-        self.assertEqual(len(dbs), 1)
-        self.assertEqual(dbs[0].allow_list[1], "192.0.1.0/24")
-        self.assertEqual(dbs[0].cluster_size, 3)
-        self.assertEqual(dbs[0].compression_type, "none")
-        self.assertEqual(dbs[0].encrypted, False)
-        self.assertEqual(dbs[0].engine, "mongodb")
-        self.assertEqual(
-            dbs[0].hosts.primary, "lin-0000-0000.servers.linodedb.net"
-        )
-        self.assertEqual(dbs[0].hosts.secondary, None)
-        self.assertEqual(len(dbs[0].peers), 3)
-        self.assertEqual(dbs[0].id, 123)
-        self.assertEqual(dbs[0].region, "us-east")
-        self.assertEqual(dbs[0].updates.duration, 3)
-        self.assertEqual(dbs[0].version, "4.4.10")
-
-    def test_create(self):
-        """
-        Test that MongoDB databases can be created
-        """
-
-        with self.mock_post("/databases/mongodb/instances") as m:
-            # We don't care about errors here; we just want to
-            # validate the request.
-            try:
-                self.client.database.mongodb_create(
-                    "cool",
-                    "us-southeast",
-                    "mongodb/4.4.10",
-                    "g6-standard-1",
-                    cluster_size=3,
-                )
-            except Exception:
-                pass
-
-            self.assertEqual(m.method, "post")
-            self.assertEqual(m.call_url, "/databases/mongodb/instances")
-            self.assertEqual(m.call_data["label"], "cool")
-            self.assertEqual(m.call_data["region"], "us-southeast")
-            self.assertEqual(m.call_data["engine"], "mongodb/4.4.10")
-            self.assertEqual(m.call_data["type"], "g6-standard-1")
-            self.assertEqual(m.call_data["cluster_size"], 3)
-
-    def test_update(self):
-        """
-        Test that the MongoDB database can be updated
-        """
-
-        with self.mock_put("/databases/mongodb/instances/123") as m:
-            new_allow_list = ["192.168.0.1/32"]
-
-            db = MongoDBDatabase(self.client, 123)
-
-            db.updates.day_of_week = 2
-            db.allow_list = new_allow_list
-            db.label = "cool"
-
-            db.save()
-
-            self.assertEqual(m.method, "put")
-            self.assertEqual(m.call_url, "/databases/mongodb/instances/123")
-            self.assertEqual(m.call_data["label"], "cool")
-            self.assertEqual(m.call_data["updates"]["day_of_week"], 2)
-            self.assertEqual(m.call_data["allow_list"], new_allow_list)
-
-    def test_list_backups(self):
-        """
-        Test that MongoDB backups list properly
-        """
-
-        db = MongoDBDatabase(self.client, 123)
-        backups = db.backups
-
-        self.assertEqual(len(backups), 1)
-
-        self.assertEqual(backups[0].id, 456)
-        self.assertEqual(
-            backups[0].label, "Scheduled - 02/04/22 11:11 UTC-XcCRmI"
-        )
-        self.assertEqual(backups[0].type, "auto")
-
-    def test_create_backup(self):
-        """
-        Test that MongoDB database backups can be created
-        """
-
-        with self.mock_post("/databases/mongodb/instances/123/backups") as m:
-            db = MongoDBDatabase(self.client, 123)
-
-            # We don't care about errors here; we just want to
-            # validate the request.
-            try:
-                db.backup_create("mybackup", target="secondary")
-            except Exception:
-                pass
-
-            self.assertEqual(m.method, "post")
-            self.assertEqual(
-                m.call_url, "/databases/mongodb/instances/123/backups"
-            )
-            self.assertEqual(m.call_data["label"], "mybackup")
-            self.assertEqual(m.call_data["target"], "secondary")
-
-    def test_backup_restore(self):
-        """
-        Test that MongoDB database backups can be restored
-        """
-
-        with self.mock_post(
-            "/databases/mongodb/instances/123/backups/456/restore"
-        ) as m:
-            db = MongoDBDatabase(self.client, 123)
-
-            db.backups[0].restore()
-
-            self.assertEqual(m.method, "post")
-            self.assertEqual(
-                m.call_url,
-                "/databases/mongodb/instances/123/backups/456/restore",
-            )
-
-    def test_patch(self):
-        """
-        Test MongoDB Database patching logic.
-        """
-        with self.mock_post("/databases/mongodb/instances/123/patch") as m:
-            db = MongoDBDatabase(self.client, 123)
-
-            db.patch()
-
-            self.assertEqual(m.method, "post")
-            self.assertEqual(
-                m.call_url, "/databases/mongodb/instances/123/patch"
-            )
-
-    def test_get_ssl(self):
-        """
-        Test MongoDB SSL cert logic
-        """
-        db = MongoDBDatabase(self.client, 123)
-
-        ssl = db.ssl
-
-        self.assertEqual(ssl.ca_certificate, "LS0tLS1CRUdJ...==")
-
-    def test_get_credentials(self):
-        """
-        Test MongoDB credentials logic
-        """
-        db = MongoDBDatabase(self.client, 123)
-
-        creds = db.credentials
-
-        self.assertEqual(creds.password, "s3cur3P@ssw0rd")
-        self.assertEqual(creds.username, "linroot")
-
-    def test_reset_credentials(self):
-        """
-        Test resetting MongoDB credentials
-        """
-        with self.mock_post(
-            "/databases/mongodb/instances/123/credentials/reset"
-        ) as m:
-            db = MongoDBDatabase(self.client, 123)
-
-            db.credentials_reset()
-
-            self.assertEqual(m.method, "post")
-            self.assertEqual(
-                m.call_url, "/databases/mongodb/instances/123/credentials/reset"
-            )
diff --git a/test/objects/nodebalancers_test.py b/test/objects/nodebalancers_test.py
index 4450cdc..8220122 100644
--- a/test/objects/nodebalancers_test.py
+++ b/test/objects/nodebalancers_test.py
@@ -1,6 +1,10 @@
 from test.base import ClientBaseCase
 
-from linode_api4.objects import NodeBalancerConfig, NodeBalancerNode
+from linode_api4.objects import (
+    NodeBalancer,
+    NodeBalancerConfig,
+    NodeBalancerNode,
+)
 from linode_api4.objects.base import MappedObject
 
 
@@ -128,3 +132,65 @@ class NodeBalancerNodeTest(ClientBaseCase):
             self.assertEqual(
                 m.call_url, "/nodebalancers/123456/configs/65432/nodes/54321"
             )
+
+    def test_config_rebuild(self):
+        """
+        Test that you can rebuild the cofig of a node balancer.
+        """
+        config_rebuild_url = "/nodebalancers/12345/configs/4567/rebuild"
+        with self.mock_post(config_rebuild_url) as m:
+            nb = NodeBalancer(self.client, 12345)
+            nodes = [
+                {
+                    "id": 54321,
+                    "address": "192.168.210.120:80",
+                    "label": "node1",
+                    "weight": 50,
+                    "mode": "accept",
+                }
+            ]
+
+            result = nb.config_rebuild(
+                4567,
+                nodes,
+                port=1234,
+                protocol="https",
+                algorithm="roundrobin",
+            )
+            self.assertIsNotNone(result)
+            self.assertEqual(result.id, 4567)
+            self.assertEqual(result.nodebalancer_id, 12345)
+            self.assertEqual(m.call_url, config_rebuild_url)
+            self.assertEqual(
+                m.call_data,
+                {
+                    "port": 1234,
+                    "protocol": "https",
+                    "algorithm": "roundrobin",
+                    "nodes": [
+                        {
+                            "id": 54321,
+                            "address": "192.168.210.120:80",
+                            "label": "node1",
+                            "weight": 50,
+                            "mode": "accept",
+                        },
+                    ],
+                },
+            )
+
+    def test_statistics(self):
+        """
+        Test that you can get the statistics about the requested NodeBalancer.
+        """
+        statistics_url = "/nodebalancers/12345/stats"
+        with self.mock_get(statistics_url) as m:
+            nb = NodeBalancer(self.client, 12345)
+            result = nb.statistics()
+
+            self.assertIsNotNone(result)
+            self.assertEqual(
+                result.title,
+                "linode.com - balancer12345 (12345) - day (5 min avg)",
+            )
+            self.assertEqual(m.call_url, statistics_url)
diff --git a/test/objects/object_storage_test.py b/test/objects/object_storage_test.py
new file mode 100644
index 0000000..a2c9219
--- /dev/null
+++ b/test/objects/object_storage_test.py
@@ -0,0 +1,259 @@
+from datetime import datetime
+from test.base import ClientBaseCase
+
+from linode_api4.objects import (
+    ObjectStorageACL,
+    ObjectStorageBucket,
+    ObjectStorageCluster,
+)
+
+
+class ObjectStorageTest(ClientBaseCase):
+    """
+    Test the methods of the ObjectStorage
+    """
+
+    def test_object_storage_bucket_api_get(self):
+        object_storage_bucket_api_get_url = (
+            "/object-storage/buckets/us-east-1/example-bucket"
+        )
+        with self.mock_get(object_storage_bucket_api_get_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            self.assertEqual(object_storage_bucket.cluster, "us-east-1")
+            self.assertEqual(object_storage_bucket.label, "example-bucket")
+            self.assertEqual(
+                object_storage_bucket.created,
+                datetime(
+                    year=2019, month=1, day=1, hour=1, minute=23, second=45
+                ),
+            )
+            self.assertEqual(
+                object_storage_bucket.hostname,
+                "example-bucket.us-east-1.linodeobjects.com",
+            )
+            self.assertEqual(object_storage_bucket.objects, 4)
+            self.assertEqual(object_storage_bucket.size, 188318981)
+            self.assertEqual(m.call_url, object_storage_bucket_api_get_url)
+
+    def test_object_storage_bucket_delete(self):
+        object_storage_bucket_delete_url = (
+            "/object-storage/buckets/us-east-1/example-bucket"
+        )
+        with self.mock_delete() as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            object_storage_bucket.delete()
+            self.assertEqual(m.call_url, object_storage_bucket_delete_url)
+
+    def test_bucket_access_modify(self):
+        """
+        Test that you can modify bucket access settings.
+        """
+        bucket_access_modify_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/access"
+        )
+        with self.mock_post({}) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            object_storage_bucket.access_modify(ObjectStorageACL.PRIVATE, True)
+            self.assertEqual(
+                m.call_data,
+                {
+                    "acl": "private",
+                    "cors_enabled": True,
+                },
+            )
+            self.assertEqual(m.call_url, bucket_access_modify_url)
+
+    def test_bucket_access_update(self):
+        """
+        Test that you can update bucket access settings.
+        """
+        bucket_access_update_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/access"
+        )
+        with self.mock_put({}) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            object_storage_bucket.access_update(ObjectStorageACL.PRIVATE, True)
+            self.assertEqual(
+                m.call_data,
+                {
+                    "acl": "private",
+                    "cors_enabled": True,
+                },
+            )
+            self.assertEqual(m.call_url, bucket_access_update_url)
+
+    def test_buckets_in_cluster(self):
+        """
+        Test that Object Storage Buckets in a specified cluster can be reterived
+        """
+        buckets_in_cluster_url = "/object-storage/buckets/us-east-1"
+        with self.mock_get(buckets_in_cluster_url) as m:
+            cluster = ObjectStorageCluster(self.client, "us-east-1")
+            buckets = cluster.buckets_in_cluster()
+            self.assertIsNotNone(buckets)
+            bucket = buckets[0]
+
+            self.assertEqual(m.call_url, buckets_in_cluster_url)
+            self.assertEqual(bucket.cluster, "us-east-1")
+            self.assertEqual(
+                bucket.created,
+                datetime(
+                    year=2019, month=1, day=1, hour=1, minute=23, second=45
+                ),
+            )
+            self.assertEqual(
+                bucket.hostname, "example-bucket.us-east-1.linodeobjects.com"
+            )
+            self.assertEqual(bucket.label, "example-bucket")
+            self.assertEqual(bucket.objects, 4)
+            self.assertEqual(bucket.size, 188318981)
+
+    def test_ssl_cert_delete(self):
+        """
+        Test that you can delete the TLS/SSL certificate and private key of a bucket.
+        """
+        ssl_cert_delete_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/ssl"
+        )
+        with self.mock_delete() as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            object_storage_bucket.ssl_cert_delete()
+            self.assertEqual(m.call_url, ssl_cert_delete_url)
+
+    def test_ssl_cert(self):
+        """
+        Test tha you can get a bool value indicating if this bucket
+        has a corresponding TLS/SSL certificate.
+        """
+        ssl_cert_url = "/object-storage/buckets/us-east-1/example-bucket/ssl"
+        with self.mock_get(ssl_cert_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            result = object_storage_bucket.ssl_cert()
+            self.assertIsNotNone(result)
+            self.assertEqual(m.call_url, ssl_cert_url)
+            self.assertEqual(result.ssl, True)
+
+    def test_ssl_cert_upload(self):
+        """
+        Test that you can upload a TLS/SSL cert.
+        """
+        ssl_cert_upload_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/ssl"
+        )
+        with self.mock_post(ssl_cert_upload_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            result = object_storage_bucket.ssl_cert_upload(
+                "-----BEGIN CERTIFICATE-----\nCERTIFICATE_INFORMATION\n-----END CERTIFICATE-----",
+                "-----BEGIN PRIVATE KEY-----\nPRIVATE_KEY_INFORMATION\n-----END PRIVATE KEY-----",
+            )
+            self.assertIsNotNone(result)
+            self.assertEqual(m.call_url, ssl_cert_upload_url)
+            self.assertEqual(result.ssl, True)
+            self.assertEqual(
+                m.call_data,
+                {
+                    "certificate": "-----BEGIN CERTIFICATE-----\nCERTIFICATE_INFORMATION\n-----END CERTIFICATE-----",
+                    "private_key": "-----BEGIN PRIVATE KEY-----\nPRIVATE_KEY_INFORMATION\n-----END PRIVATE KEY-----",
+                },
+            )
+
+    def test_contents(self):
+        """
+        Test that you can get the contents of a bucket.
+        """
+        bucket_contents_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/object-list"
+        )
+        with self.mock_get(bucket_contents_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            contents = object_storage_bucket.contents()
+            self.assertIsNotNone(contents)
+            content = contents[0]
+
+            self.assertEqual(m.call_url, bucket_contents_url)
+            self.assertEqual(content.etag, "9f254c71e28e033bf9e0e5262e3e72ab")
+            self.assertEqual(content.is_truncated, True)
+            self.assertEqual(content.last_modified, "2019-01-01T01:23:45")
+            self.assertEqual(content.name, "example")
+            self.assertEqual(
+                content.next_marker,
+                "bd021c21-e734-4823-97a4-58b41c2cd4c8.892602.184",
+            )
+            self.assertEqual(
+                content.owner, "bfc70ab2-e3d4-42a4-ad55-83921822270c"
+            )
+            self.assertEqual(content.size, 123)
+            self.assertEqual(
+                m.call_data,
+                {
+                    "page_size": 100,
+                },
+            )
+
+    def test_object_acl_config(self):
+        """
+        Test that you can view an Object’s configured Access Control List (ACL) in this Object Storage bucket.
+        """
+        object_acl_config_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/object-acl"
+        )
+        with self.mock_get(object_acl_config_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            acl = object_storage_bucket.object_acl_config("example")
+            self.assertEqual(m.call_url, object_acl_config_url)
+            self.assertEqual(acl.acl, "public-read")
+            self.assertEqual(
+                acl.acl_xml, "<AccessControlPolicy>...</AccessControlPolicy>"
+            )
+            self.assertEqual(
+                m.call_data,
+                {
+                    "name": "example",
+                },
+            )
+
+    def test_object_acl_config_update(self):
+        """
+        Test that you can update an Object’s configured Access Control List (ACL) in this Object Storage bucket.
+        """
+        object_acl_config_update_url = (
+            "/object-storage/buckets/us-east-1/example-bucket/object-acl"
+        )
+        with self.mock_put(object_acl_config_update_url) as m:
+            object_storage_bucket = ObjectStorageBucket(
+                self.client, "example-bucket", "us-east-1"
+            )
+            acl = object_storage_bucket.object_acl_config_update(
+                ObjectStorageACL.PUBLIC_READ,
+                "example",
+            )
+            self.assertEqual(m.call_url, object_acl_config_update_url)
+            self.assertEqual(acl.acl, "public-read")
+            self.assertEqual(
+                acl.acl_xml, "<AccessControlPolicy>...</AccessControlPolicy>"
+            )
+            self.assertEqual(
+                m.call_data,
+                {
+                    "acl": "public-read",
+                    "name": "example",
+                },
+            )