diff --git a/docs/conf.py b/docs/conf.py index 78f735c33a..38a4366116 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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, diff --git a/docs/develop/api_doc.rst b/docs/develop/api_doc.rst index 78acea4348..0dfcfb7c27 100644 --- a/docs/develop/api_doc.rst +++ b/docs/develop/api_doc.rst @@ -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: diff --git a/docs/develop/objects.rst b/docs/develop/objects.rst index 754f4e6932..074929ad62 100644 --- a/docs/develop/objects.rst +++ b/docs/develop/objects.rst @@ -8,4 +8,21 @@ Base Objects ------------ .. automodule:: nailgun.objects.base - :special-members: _check_field \ No newline at end of file + + +Release-related Objects +----------------------- + +.. automodule:: nailgun.objects.release + + +Cluster-related Objects +----------------------- + +.. automodule:: nailgun.objects.cluster + + +Node-related Objects +-------------------- + +.. automodule:: nailgun.objects.node diff --git a/nailgun/nailgun/autoapidoc.py b/nailgun/nailgun/autoapidoc.py index 95010f77e7..9841d0e8b7 100644 --- a/nailgun/nailgun/autoapidoc.py +++ b/nailgun/nailgun/autoapidoc.py @@ -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, "") diff --git a/nailgun/nailgun/objects/base.py b/nailgun/nailgun/objects/base.py index f6a1098b2b..9fd45e8d00 100644 --- a/nailgun/nailgun/objects/base.py +++ b/nailgun/nailgun/objects/base.py @@ -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): diff --git a/nailgun/nailgun/objects/cluster.py b/nailgun/nailgun/objects/cluster.py index 73d816f519..58a550eeed 100644 --- a/nailgun/nailgun/objects/cluster.py +++ b/nailgun/nailgun/objects/cluster.py @@ -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 diff --git a/nailgun/nailgun/objects/node.py b/nailgun/nailgun/objects/node.py index 08e0168267..2848734fb9 100755 --- a/nailgun/nailgun/objects/node.py +++ b/nailgun/nailgun/objects/node.py @@ -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 diff --git a/nailgun/nailgun/objects/release.py b/nailgun/nailgun/objects/release.py index 326d9cd4cb..027cae4ffb 100644 --- a/nailgun/nailgun/objects/release.py +++ b/nailgun/nailgun/objects/release.py @@ -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