Nailgun Docs for base objects
Change-Id: I1f2af3b2d5c0a1cc3e078f96fa4fcd810fbb9680
This commit is contained in:
parent
61410bcf32
commit
054de90090
|
@ -15,7 +15,7 @@ import os
|
|||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('.'), "..", "nailgun"))
|
||||
autodoc_default_flags = ['members', 'inherited-members']
|
||||
autodoc_default_flags = ['members', 'show-inheritance']
|
||||
autodoc_member_order = 'bysource'
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
|
|
|
@ -8,57 +8,67 @@ Releases API
|
|||
------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.release
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Clusters API
|
||||
------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.cluster
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Nodes API
|
||||
---------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.node
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Disks API
|
||||
---------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.disks
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Network Configuration API
|
||||
-------------------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.network_configuration
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Notifications API
|
||||
-----------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.notifications
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Tasks API
|
||||
-----------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.tasks
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Logs API
|
||||
-----------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.logs
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Redhat API
|
||||
-----------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.redhat
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Version API
|
||||
-----------------
|
||||
|
||||
.. automodule:: nailgun.api.handlers.version
|
||||
:inherited-members:
|
||||
|
|
|
@ -8,4 +8,21 @@ Base Objects
|
|||
------------
|
||||
|
||||
.. automodule:: nailgun.objects.base
|
||||
:special-members: _check_field
|
||||
|
||||
|
||||
Release-related Objects
|
||||
-----------------------
|
||||
|
||||
.. automodule:: nailgun.objects.release
|
||||
|
||||
|
||||
Cluster-related Objects
|
||||
-----------------------
|
||||
|
||||
.. automodule:: nailgun.objects.cluster
|
||||
|
||||
|
||||
Node-related Objects
|
||||
--------------------
|
||||
|
||||
.. automodule:: nailgun.objects.node
|
||||
|
|
|
@ -30,7 +30,7 @@ class SampleGenerator(object):
|
|||
|
||||
@classmethod
|
||||
def gen_sample_data(cls):
|
||||
def process(app, what_, name, obj, options, lines):
|
||||
def process(app, what, name, obj, options, lines):
|
||||
if cls._ishandler(obj):
|
||||
lines.insert(0, cls.generate_handler_url_doc(obj))
|
||||
lines.insert(1, "")
|
||||
|
|
|
@ -35,16 +35,16 @@ class NailgunObject(object):
|
|||
"""Base class for objects
|
||||
"""
|
||||
|
||||
#: Serializer class for object
|
||||
serializer = BasicSerializer
|
||||
"""Serializer class for object"""
|
||||
|
||||
#: SQLAlchemy model for object
|
||||
model = None
|
||||
"""SQLAlchemy model for object"""
|
||||
|
||||
#: JSON schema for object
|
||||
schema = {
|
||||
"properties": {}
|
||||
}
|
||||
"""JSON schema for object"""
|
||||
|
||||
@classmethod
|
||||
def check_field(cls, field):
|
||||
|
@ -135,8 +135,8 @@ class NailgunCollection(object):
|
|||
"""Base class for object collections
|
||||
"""
|
||||
|
||||
#: Single object class
|
||||
single = NailgunObject
|
||||
"""Single object class"""
|
||||
|
||||
@classmethod
|
||||
def _is_iterable(cls, obj):
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Cluster-related objects and collections
|
||||
"""
|
||||
|
||||
from nailgun import consts
|
||||
|
||||
from nailgun.api.serializers.cluster import ClusterSerializer
|
||||
|
@ -33,11 +37,20 @@ from nailgun.utils import traverse
|
|||
|
||||
|
||||
class Attributes(NailgunObject):
|
||||
"""Cluster attributes object
|
||||
"""
|
||||
|
||||
#: SQLAlchemy model for Cluster attributes
|
||||
model = models.Attributes
|
||||
|
||||
@classmethod
|
||||
def generate_fields(cls, instance):
|
||||
"""Generate field values for Cluster attributes using
|
||||
generators.
|
||||
|
||||
:param instance: Attributes instance
|
||||
:returns: None
|
||||
"""
|
||||
instance.generated = traverse(
|
||||
instance.generated,
|
||||
AttributesGenerator
|
||||
|
@ -47,6 +60,13 @@ class Attributes(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def merged_attrs(cls, instance):
|
||||
"""Generates merged dict which includes generated Cluster
|
||||
attributes recursively updated by new values from editable
|
||||
attributes.
|
||||
|
||||
:param instance: Attributes instance
|
||||
:returns: dict of merged attributes
|
||||
"""
|
||||
return dict_merge(
|
||||
instance.generated,
|
||||
instance.editable
|
||||
|
@ -54,6 +74,12 @@ class Attributes(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def merged_attrs_values(cls, instance):
|
||||
"""Transforms raw dict of attributes returned by :func:`merged_attrs`
|
||||
into dict of facts for sending to orchestrator.
|
||||
|
||||
:param instance: Attributes instance
|
||||
:returns: dict of merged attributes
|
||||
"""
|
||||
attrs = cls.merged_attrs(instance)
|
||||
for group_attrs in attrs.itervalues():
|
||||
for attr, value in group_attrs.iteritems():
|
||||
|
@ -73,10 +99,16 @@ class Attributes(NailgunObject):
|
|||
|
||||
|
||||
class Cluster(NailgunObject):
|
||||
"""Cluster object
|
||||
"""
|
||||
|
||||
#: SQLAlchemy model for Cluster
|
||||
model = models.Cluster
|
||||
|
||||
#: Serializer for Cluster
|
||||
serializer = ClusterSerializer
|
||||
|
||||
#: Cluster JSON schema
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Cluster",
|
||||
|
@ -110,6 +142,20 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create(cls, data):
|
||||
"""Create Cluster instance with specified parameters in DB.
|
||||
This includes:
|
||||
|
||||
* creating Cluster attributes and generating default values \
|
||||
(see :func:`create_attributes`)
|
||||
* creating NetworkGroups for Cluster
|
||||
* adding default pending changes (see :func:`add_pending_changes`)
|
||||
* if "nodes" are specified in data then they are added to Cluster \
|
||||
(see :func:`update_nodes`)
|
||||
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Cluster instance
|
||||
"""
|
||||
|
||||
#TODO(enchantner): fix this temporary hack in clients
|
||||
if "release_id" not in data:
|
||||
release_id = data.pop("release", None)
|
||||
|
@ -146,6 +192,13 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create_attributes(cls, instance):
|
||||
"""Create attributes for current Cluster instance and
|
||||
generate default values for them
|
||||
(see :func:`Attributes.generate_fields`)
|
||||
|
||||
:param instance: Cluster instance
|
||||
:returns: None
|
||||
"""
|
||||
attributes = Attributes.create(
|
||||
{
|
||||
"editable": instance.release.attributes_metadata.get(
|
||||
|
@ -161,12 +214,23 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def get_attributes(cls, instance):
|
||||
"""Get attributes for current Cluster instance
|
||||
|
||||
:param instance: Cluster instance
|
||||
:returns: Attributes instance
|
||||
"""
|
||||
return db().query(models.Attributes).filter(
|
||||
models.Attributes.cluster_id == instance.id
|
||||
).first()
|
||||
|
||||
@classmethod
|
||||
def get_network_manager(cls, instance=None):
|
||||
"""Get network manager for Cluster instance.
|
||||
If instance is None then the default NetworkManager is returned
|
||||
|
||||
:param instance: Cluster instance
|
||||
:returns: NetworkManager/NovaNetworkManager/NeutronManager
|
||||
"""
|
||||
if not instance:
|
||||
from nailgun.network.manager import NetworkManager
|
||||
return NetworkManager
|
||||
|
@ -180,6 +244,16 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def add_pending_changes(cls, instance, changes_type, node_id=None):
|
||||
"""Add pending changes for current Cluster.
|
||||
If node_id is specified then links created changes with node.
|
||||
|
||||
:param instance: Cluster instance
|
||||
:param changes_type: name of changes to add
|
||||
:param node_id: node id for changes
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
#TODO(enchantner): check if node belongs to cluster
|
||||
ex_chs = db().query(models.ClusterChanges).filter_by(
|
||||
cluster=instance,
|
||||
name=changes_type
|
||||
|
@ -202,6 +276,14 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def clear_pending_changes(cls, instance, node_id=None):
|
||||
"""Clear pending changes for current Cluster.
|
||||
If node_id is specified then only clears changes connected
|
||||
to this node.
|
||||
|
||||
:param instance: Cluster instance
|
||||
:param node_id: node id for changes
|
||||
:returns: None
|
||||
"""
|
||||
chs = db().query(models.ClusterChanges).filter_by(
|
||||
cluster_id=instance.id
|
||||
)
|
||||
|
@ -212,6 +294,14 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update(cls, instance, data):
|
||||
"""Update Cluster object instance with specified parameters in DB.
|
||||
If "nodes" are specified in data then they will replace existing ones
|
||||
(see :func:`update_nodes`)
|
||||
|
||||
:param instance: Cluster instance
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Cluster instance
|
||||
"""
|
||||
nodes = data.pop("nodes", None)
|
||||
super(Cluster, cls).update(instance, data)
|
||||
if nodes is not None:
|
||||
|
@ -220,6 +310,14 @@ class Cluster(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update_nodes(cls, instance, nodes_ids):
|
||||
"""Update Cluster nodes by specified node IDs.
|
||||
Nodes with specified IDs will replace existing ones in Cluster
|
||||
|
||||
:param instance: Cluster instance
|
||||
:param nodes_ids: list of nodes ids
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
# TODO(NAME): sepatate nodes
|
||||
#for deletion and addition by set().
|
||||
new_nodes = []
|
||||
|
@ -256,5 +354,8 @@ class Cluster(NailgunObject):
|
|||
|
||||
|
||||
class ClusterCollection(NailgunCollection):
|
||||
"""Cluster collection
|
||||
"""
|
||||
|
||||
#: Single Cluster object class
|
||||
single = Cluster
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Node-related objects and collections
|
||||
"""
|
||||
|
||||
import traceback
|
||||
|
||||
from datetime import datetime
|
||||
|
@ -34,10 +38,16 @@ from nailgun.objects import Notification
|
|||
|
||||
|
||||
class Node(NailgunObject):
|
||||
"""Node object
|
||||
"""
|
||||
|
||||
#: SQLAlchemy model for Node
|
||||
model = models.Node
|
||||
|
||||
#: Serializer for Node
|
||||
serializer = NodeSerializer
|
||||
|
||||
#: Node JSON schema
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Node",
|
||||
|
@ -75,6 +85,12 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def get_by_mac_or_uid(cls, mac=None, node_uid=None):
|
||||
"""Get Node instance by MAC or ID.
|
||||
|
||||
:param mac: MAC address as string
|
||||
:param node_uid: Node ID
|
||||
:returns: Node instance
|
||||
"""
|
||||
node = None
|
||||
if not mac and not node_uid:
|
||||
return node
|
||||
|
@ -88,6 +104,11 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def search_by_interfaces(cls, interfaces):
|
||||
"""Search for instance using MACs on interfaces
|
||||
|
||||
:param interfaces: dict of Node interfaces
|
||||
:returns: Node instance
|
||||
"""
|
||||
return db().query(cls.model).join(
|
||||
models.NodeNICInterface,
|
||||
cls.model.nic_interfaces
|
||||
|
@ -99,6 +120,23 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create(cls, data):
|
||||
"""Create Node instance with specified parameters in DB.
|
||||
This includes:
|
||||
|
||||
* generating its name by MAC (if name is not specified in data)
|
||||
* adding node to Cluster (if cluster_id is not None in data) \
|
||||
(see :func:`add_into_cluster`) with specified roles \
|
||||
(see :func:`update_roles` and :func:`update_pending_roles`)
|
||||
* creating interfaces for Node in DB (see :func:`update_interfaces`)
|
||||
* creating default Node attributes (see :func:`create_attributes`)
|
||||
* creating default volumes allocation for Node \
|
||||
(see :func:`update_volumes`)
|
||||
* creating Notification about newly discovered Node \
|
||||
(see :func:`create_discover_notification`)
|
||||
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Node instance
|
||||
"""
|
||||
if "name" not in data:
|
||||
data["name"] = "Untitled ({0})".format(
|
||||
data['mac'][-5:].lower()
|
||||
|
@ -143,6 +181,11 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create_attributes(cls, instance):
|
||||
"""Create attributes for Node instance
|
||||
|
||||
:param instance: Node instance
|
||||
:returns: NodeAttributes instance
|
||||
"""
|
||||
new_attributes = models.NodeAttributes()
|
||||
instance.attributes = new_attributes
|
||||
db().add(new_attributes)
|
||||
|
@ -152,12 +195,24 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update_interfaces(cls, instance):
|
||||
"""Update interfaces for Node instance using Cluster
|
||||
network manager (see :func:`get_network_manager`)
|
||||
|
||||
:param instance: Node instance
|
||||
:returns: None
|
||||
"""
|
||||
Cluster.get_network_manager(
|
||||
instance.cluster
|
||||
).update_interfaces_info(instance)
|
||||
|
||||
@classmethod
|
||||
def update_volumes(cls, instance):
|
||||
"""Update volumes for Node instance.
|
||||
Adds pending "disks" changes for Cluster which Node belongs to
|
||||
|
||||
:param instance: Node instance
|
||||
:returns: None
|
||||
"""
|
||||
attrs = instance.attributes
|
||||
if not attrs:
|
||||
attrs = cls.create_attributes(instance)
|
||||
|
@ -191,6 +246,11 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create_discover_notification(cls, instance):
|
||||
"""Create notification about discovering new Node
|
||||
|
||||
:param instance: Node instance
|
||||
:returns: None
|
||||
"""
|
||||
try:
|
||||
# we use multiplier of 1024 because there are no problems here
|
||||
# with unfair size calculation
|
||||
|
@ -233,6 +293,23 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update(cls, instance, data):
|
||||
"""Update Node instance with specified parameters in DB.
|
||||
This includes:
|
||||
|
||||
* adding node to Cluster (if cluster_id is not None in data) \
|
||||
(see :func:`add_into_cluster`)
|
||||
* updating roles for Node if it belongs to Cluster \
|
||||
(see :func:`update_roles` and :func:`update_pending_roles`)
|
||||
* removing node from Cluster (if cluster_id is None in data) \
|
||||
(see :func:`remove_from_cluster`)
|
||||
* updating interfaces for Node in DB (see :func:`update_interfaces`)
|
||||
* creating default Node attributes (see :func:`create_attributes`)
|
||||
* updating volumes allocation for Node using Cluster's Release \
|
||||
metadata (see :func:`update_volumes`)
|
||||
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Node instance
|
||||
"""
|
||||
data.pop("id", None)
|
||||
|
||||
roles = data.pop("roles", None)
|
||||
|
@ -300,6 +377,13 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update_roles(cls, instance, new_roles):
|
||||
"""Update roles for Node instance.
|
||||
Logs an error if node doesn't belong to Cluster
|
||||
|
||||
:param instance: Node instance
|
||||
:param new_roles: list of new role names
|
||||
:returns: None
|
||||
"""
|
||||
if not instance.cluster_id:
|
||||
logger.warning(
|
||||
u"Attempting to assign roles to node "
|
||||
|
@ -319,6 +403,13 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update_pending_roles(cls, instance, new_pending_roles):
|
||||
"""Update pending_roles for Node instance.
|
||||
Logs an error if node doesn't belong to Cluster
|
||||
|
||||
:param instance: Node instance
|
||||
:param new_pending_roles: list of new pending role names
|
||||
:returns: None
|
||||
"""
|
||||
if not instance.cluster_id:
|
||||
logger.warning(
|
||||
u"Attempting to assign pending roles to node "
|
||||
|
@ -354,6 +445,13 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def add_into_cluster(cls, instance, cluster_id):
|
||||
"""Adds Node to Cluster by its ID.
|
||||
Also assigns networks by default for Node.
|
||||
|
||||
:param instance: Node instance
|
||||
:param cluster_id: Cluster ID
|
||||
:returns: None
|
||||
"""
|
||||
instance.cluster_id = cluster_id
|
||||
db().flush()
|
||||
db().refresh(instance)
|
||||
|
@ -362,6 +460,13 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def get_network_manager(cls, instance=None):
|
||||
"""Get network manager for Node instance.
|
||||
If instance is None then default NetworkManager is returned
|
||||
|
||||
:param instance: Node instance
|
||||
:param cluster_id: Cluster ID
|
||||
:returns: None
|
||||
"""
|
||||
if not instance.cluster:
|
||||
from nailgun.network.manager import NetworkManager
|
||||
return NetworkManager
|
||||
|
@ -370,6 +475,14 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def remove_from_cluster(cls, instance):
|
||||
"""Remove Node from Cluster.
|
||||
Also drops networks assignment for Node and clears both
|
||||
roles and pending roles
|
||||
|
||||
:param instance: Node instance
|
||||
:param cluster_id: Cluster ID
|
||||
:returns: None
|
||||
"""
|
||||
Cluster.clear_pending_changes(
|
||||
instance.cluster,
|
||||
node_id=instance.id
|
||||
|
@ -385,6 +498,13 @@ class Node(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def to_dict(cls, instance, fields=None):
|
||||
"""Serialize Node instance to Python dict.
|
||||
Adds "network_data" field which includes all network data for Node
|
||||
|
||||
:param instance: Node instance
|
||||
:param fields: exact fields to serialize
|
||||
:returns: serialized Node as dictionary
|
||||
"""
|
||||
node_dict = super(Node, cls).to_dict(instance, fields=fields)
|
||||
net_manager = Cluster.get_network_manager(instance.cluster)
|
||||
ips_mapped = net_manager.get_grouped_ips_by_node()
|
||||
|
@ -399,5 +519,8 @@ class Node(NailgunObject):
|
|||
|
||||
|
||||
class NodeCollection(NailgunCollection):
|
||||
"""Node collection
|
||||
"""
|
||||
|
||||
#: Single Node object class
|
||||
single = Node
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Release object and collection
|
||||
"""
|
||||
|
||||
from sqlalchemy import not_
|
||||
|
||||
from nailgun import consts
|
||||
|
@ -30,10 +34,16 @@ from nailgun.objects import NailgunObject
|
|||
|
||||
|
||||
class Release(NailgunObject):
|
||||
"""Release object
|
||||
"""
|
||||
|
||||
#: SQLAlchemy model for Release
|
||||
model = DBRelease
|
||||
|
||||
#: Serializer for Release
|
||||
serializer = ReleaseSerializer
|
||||
|
||||
#: Release JSON schema
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Release",
|
||||
|
@ -65,6 +75,13 @@ class Release(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def create(cls, data):
|
||||
"""Create Release instance with specified parameters in DB.
|
||||
Corresponding roles are created in DB using names specified
|
||||
in "roles" field. See :func:`update_roles`
|
||||
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Release instance
|
||||
"""
|
||||
roles = data.pop("roles", None)
|
||||
new_obj = super(Release, cls).create(data)
|
||||
if roles:
|
||||
|
@ -73,6 +90,14 @@ class Release(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update(cls, instance, data):
|
||||
"""Update existing Release instance with specified parameters.
|
||||
Corresponding roles are updated in DB using names specified
|
||||
in "roles" field. See :func:`update_roles`
|
||||
|
||||
:param instance: Release instance
|
||||
:param data: dictionary of key-value pairs as object fields
|
||||
:returns: Release instance
|
||||
"""
|
||||
roles = data.pop("roles", None)
|
||||
super(Release, cls).update(instance, data)
|
||||
if roles is not None:
|
||||
|
@ -81,6 +106,16 @@ class Release(NailgunObject):
|
|||
|
||||
@classmethod
|
||||
def update_roles(cls, instance, roles):
|
||||
"""Update existing Release instance with specified roles.
|
||||
Previous ones are deleted.
|
||||
|
||||
IMPORTANT NOTE: attempting to remove roles that are already
|
||||
assigned to nodes will lead to an Exception.
|
||||
|
||||
:param instance: Release instance
|
||||
:param roles: list of new roles names
|
||||
:returns: None
|
||||
"""
|
||||
db().query(DBRole).filter(
|
||||
not_(DBRole.name.in_(roles))
|
||||
).filter(
|
||||
|
@ -101,5 +136,8 @@ class Release(NailgunObject):
|
|||
|
||||
|
||||
class ReleaseCollection(NailgunCollection):
|
||||
"""Release collection
|
||||
"""
|
||||
|
||||
#: Single Release object class
|
||||
single = Release
|
||||
|
|
Loading…
Reference in New Issue