# -*- coding: utf-8 -*- # Copyright 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Node-related objects and collections """ import copy import itertools import operator from oslo_serialization import jsonutils import traceback from datetime import datetime from netaddr import IPAddress from netaddr import IPNetwork from sqlalchemy.orm import joinedload from sqlalchemy.orm import subqueryload_all from nailgun import consts from nailgun.objects.serializers.node import NodeSerializer from nailgun.db import db from nailgun.db.sqlalchemy import models from nailgun.errors import errors from nailgun.extensions import fire_callback_on_node_collection_delete from nailgun.extensions import fire_callback_on_node_create from nailgun.extensions import fire_callback_on_node_delete from nailgun.extensions import fire_callback_on_node_reset from nailgun.extensions import fire_callback_on_node_update from nailgun.logger import logger from nailgun.objects import Cluster from nailgun.objects import NailgunCollection from nailgun.objects import NailgunObject from nailgun.objects import Notification from nailgun.settings import settings from nailgun.network.template import NetworkTemplate class Node(NailgunObject): """Node object""" #: SQLAlchemy model for Node model = models.Node #: Serializer for Node serializer = NodeSerializer @classmethod def delete(cls, instance): fire_callback_on_node_delete(instance) super(Node, cls).delete(instance) @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 q = db().query(cls.model) if mac: node = q.filter_by(mac=mac.lower()).first() else: node = q.get(node_uid) return node @classmethod def get_by_hostname(cls, hostname, cluster_id): """Get Node instance by hostname. :param hostname: hostname as string :param cluster_id: Node will be searched \ only within the cluster with this ID. :returns: Node instance """ if not hostname: return None q = db().query(cls.model).filter_by( hostname=hostname, cluster_id=cluster_id) return q.first() @classmethod def get_by_meta(cls, meta): """Search for instance using mac, node id or interfaces :param meta: dict with nodes metadata :returns: Node instance """ node = cls.get_by_mac_or_uid( mac=meta.get('mac'), node_uid=meta.get('id')) if not node: can_search_by_ifaces = all([ meta.get('meta'), meta['meta'].get('interfaces')]) if can_search_by_ifaces: node = cls.search_by_interfaces(meta['meta']['interfaces']) return node @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 ).filter( models.NodeNICInterface.mac.in_( [n["mac"].lower() for n in interfaces] ) ).first() @classmethod def should_have_public_with_ip(cls, instance): """Returns True if node should have IP belonging to Public network :param instance: Node DB instance :returns: True when node has Public network """ if Cluster.should_assign_public_to_all_nodes(instance.cluster): return True roles = itertools.chain(instance.roles, instance.pending_roles) roles_metadata = Cluster.get_roles(instance.cluster) for role in roles: if roles_metadata.get(role, {}).get('public_ip_required'): return True return False @classmethod def should_have_public(cls, instance): """Determine whether this node should be connected to Public network, no matter with or without an IP address assigned from that network For example Neutron DVR does require Public network access on compute nodes, but does not require IP address assigned to external bridge. :param instance: Node DB instance :returns: True when node has Public network """ if cls.should_have_public_with_ip(instance): return True dvr_enabled = Cluster.neutron_dvr_enabled(instance.cluster) if dvr_enabled: roles = itertools.chain(instance.roles, instance.pending_roles) roles_metadata = Cluster.get_roles(instance.cluster) for role in roles: if roles_metadata.get(role, {}).get('public_for_dvr_required'): return True return False @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 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() ) data["timestamp"] = datetime.now() data.pop("id", None) # TODO(enchantner): fix this temporary hack in clients if "cluster_id" not in data and "cluster" in data: cluster_id = data.pop("cluster", None) data["cluster_id"] = cluster_id roles = data.pop("roles", None) pending_roles = data.pop("pending_roles", None) primary_roles = data.pop("primary_roles", None) new_node_meta = data.pop("meta", {}) new_node_cluster_id = data.pop("cluster_id", None) new_node = super(Node, cls).create(data) new_node.create_meta(new_node_meta) if 'hostname' not in data: new_node.hostname = \ cls.get_unique_hostname(new_node, new_node_cluster_id) db().flush() # Add interfaces for node from 'meta'. if new_node.meta and new_node.meta.get('interfaces'): cls.update_interfaces(new_node) # adding node into cluster if new_node_cluster_id: cls.add_into_cluster(new_node, new_node_cluster_id) # updating roles if roles is not None: cls.update_roles(new_node, roles) if pending_roles is not None: cls.update_pending_roles(new_node, pending_roles) if primary_roles is not None: cls.update_primary_roles(new_node, primary_roles) # creating attributes cls.create_attributes(new_node) cls.create_discover_notification(new_node) if new_node.ip: cls.check_ip_belongs_to_any_admin_network(new_node) fire_callback_on_node_create(new_node) return new_node @classmethod def set_error_status_and_file_notification(cls, instance, etype, emessage): instance.status = consts.NODE_STATUSES.error instance.error_type = etype instance.error_msg = emessage db().flush() Notification.create({ "topic": consts.NOTIFICATION_TOPICS.error, "message": instance.error_msg, "node_id": instance.id }) @classmethod def check_ip_belongs_to_any_admin_network(cls, instance, new_ip=None): """Checks that node's IP belongs to any of Admin networks IP ranges. Node can be inside or out of a cluster. Set node to error and file a notification if node's IP does not belong to any of Admin networks. :param instance: node instance :param new_ip: new IP for a node (got from Nailgun agent) :return: True if IP belongs to any of Admin networks """ ip = new_ip or instance.ip nm = Cluster.get_network_manager(instance.cluster) match = nm.check_ips_belong_to_admin_ranges([ip]) if not match: cls.set_error_status_and_file_notification( instance, consts.NODE_ERRORS.discover, "Node '{0}' has IP '{1}' that does not match any Admin " "network".format(instance.hostname, ip) ) return match @classmethod def check_ip_belongs_to_own_admin_network(cls, instance, new_ip=None): """Checks that node's IP belongs to node's Admin network IP ranges. Node should be inside a cluster. Set node to error and file a notification if node's IP does not belong to its Admin network. :param instance: node instance :param new_ip: new IP for a node (got from Nailgun agent) :return: True if IP belongs to node's Admin network """ ip = new_ip or instance.ip nm = Cluster.get_network_manager(instance.cluster) admin_ng = nm.get_admin_network_group(instance.id) match = nm.is_same_network(ip, admin_ng.cidr) if not match: cls.set_error_status_and_file_notification( instance, consts.NODE_ERRORS.discover, "Node '{0}' has IP '{1}' that does not match its own Admin " "network '{2}'".format(instance.hostname, ip, admin_ng.cidr) ) return match @classmethod def assign_group(cls, instance): if instance.group_id is None and instance.ip: admin_ngs = db().query(models.NetworkGroup).filter_by( name="fuelweb_admin") ip = IPAddress(instance.ip) for ng in admin_ngs: if ip in IPNetwork(ng.cidr): instance.group_id = ng.group_id break if not instance.group_id: instance.group_id = Cluster.get_default_group(instance.cluster).id db().add(instance) db().flush() @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) db().add(instance) db().flush() return new_attributes @classmethod def hardware_info_locked(cls, instance): """Returns true if update of hardware information is not allowed. It is not allowed during provision/deployment, after successful provision/deployment and during node removal. """ return instance.status not in ( consts.NODE_STATUSES.discover, consts.NODE_STATUSES.error, ) @classmethod def update_interfaces(cls, instance, update_by_agent=False): """Update interfaces for Node instance using Cluster network manager (see :func:`get_network_manager`) :param instance: Node instance :returns: None """ try: network_manager = Cluster.get_network_manager(instance.cluster) network_manager.update_interfaces_info(instance, update_by_agent) db().refresh(instance) except errors.InvalidInterfacesInfo as exc: logger.warning( "Failed to update interfaces for node '%s' - invalid info " "in meta: %s", instance.human_readable_name, exc.message ) logger.warning(traceback.format_exc()) @classmethod def set_vms_conf(cls, instance, vms_conf): """Set vms_conf for Node instance from JSON data. :param instance: Node instance :param volumes_data: JSON with new vms_conf data :returns: None """ db().query(models.NodeAttributes).filter_by( node_id=instance.id).update({'vms_conf': vms_conf}) db().flush() db().refresh(instance) @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 ram = str(round(float( instance.meta['memory']['total']) / 1073741824, 1)) + " GB RAM" except Exception: logger.warning(traceback.format_exc()) ram = "unknown RAM" try: # we use multiplier of 1000 because disk vendors specify HDD size # in terms of decimal capacity. Sources: # http://knowledge.seagate.com/articles/en_US/FAQ/172191en # http://physics.nist.gov/cuu/Units/binary.html hd_size = round( float( sum( [d["size"] for d in instance.meta["disks"]] ) / 1000000000 ), 1 ) # if HDD > 100 GB we show it's size in TB if hd_size > 100: hd_size = str(hd_size / 1000) + " TB HDD" else: hd_size = str(hd_size) + " GB HDD" except Exception: logger.warning(traceback.format_exc()) hd_size = "unknown HDD" cores = str(instance.meta.get('cpu', {}).get('total', "unknown")) Notification.create({ "topic": "discover", "message": u"New node is discovered: " u"{0} CPUs / {1} / {2} ".format(cores, ram, hd_size), "node_id": instance.id }) @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`) :param data: dictionary of key-value pairs as object fields :returns: Node instance """ data.pop("id", None) data.pop("network_data", None) roles = data.pop("roles", None) pending_roles = data.pop("pending_roles", None) new_meta = data.pop("meta", None) update_by_agent = data.pop("is_agent", False) disks_changed = None if new_meta and "disks" in new_meta and "disks" in instance.meta: key = operator.itemgetter("name") new_disks = sorted(new_meta["disks"], key=key) old_disks = sorted(instance.meta["disks"], key=key) disks_changed = (new_disks != old_disks) # TODO(enchantner): fix this temporary hack in clients if "cluster_id" not in data and "cluster" in data: cluster_id = data.pop("cluster", None) data["cluster_id"] = cluster_id if new_meta: instance.update_meta(new_meta) # The call to update_interfaces will execute a select query for # the current instance. This appears to overwrite the object in the # current session and we lose the meta changes. db().flush() if cls.hardware_info_locked(instance): logger.debug("Interfaces are locked for update on node %s", instance.human_readable_name) else: instance.ip = data.pop("ip", None) or instance.ip instance.mac = data.pop("mac", None) or instance.mac db().flush() cls.update_interfaces(instance, update_by_agent) cluster_changed = False add_to_cluster = False if "cluster_id" in data: new_cluster_id = data.pop("cluster_id") if instance.cluster_id: if new_cluster_id is None: # removing node from cluster cluster_changed = True cls.remove_from_cluster(instance) elif new_cluster_id != instance.cluster_id: # changing node cluster to another # (is currently not allowed) raise errors.CannotUpdate( u"Changing cluster on the fly is not allowed" ) else: if new_cluster_id is not None: # assigning node to cluster cluster_changed = True add_to_cluster = True instance.cluster_id = new_cluster_id if "group_id" in data: new_group_id = data.pop("group_id") if instance.group_id != new_group_id: nm = Cluster.get_network_manager(instance.cluster) nm.clear_assigned_networks(instance) nm.clear_bond_configuration(instance) instance.group_id = new_group_id add_to_cluster = True # calculating flags roles_changed = ( roles is not None and set(roles) != set(instance.roles) ) pending_roles_changed = ( pending_roles is not None and set(pending_roles) != set(instance.pending_roles) ) super(Node, cls).update(instance, data) if roles_changed: cls.update_roles(instance, roles) if pending_roles_changed: cls.update_pending_roles(instance, pending_roles) if add_to_cluster: cls.add_into_cluster(instance, instance.cluster_id) if any(( roles_changed, pending_roles_changed, cluster_changed, disks_changed, )) and instance.status not in ( consts.NODE_STATUSES.provisioning, consts.NODE_STATUSES.deploying ): # TODO(eli): we somehow should move this # condition into extension, in order to do # that probably we will have to create separate # table to keep disks which were used to create # volumes mapping. # Should be solved as a part of blueprint # https://blueprints.launchpad.net/fuel/+spec # /volume-manager-refactoring fire_callback_on_node_update(instance) return instance @classmethod def reset_to_discover(cls, instance): """Flush database objects which is not consistent with actual node configuration in the event of resetting node to discover state :param instance: Node database object :returns: None """ node_data = { "online": False, "status": consts.NODE_STATUSES.discover, "pending_addition": True, "pending_deletion": False, } cls.update(instance, node_data) cls.move_roles_to_pending_roles(instance) # when node reseted to discover: # - cobbler system is deleted # - mac to ip mapping from dnsmasq.conf is deleted # imho we need to revert node to original state, as it was # added to cluster (without any additonal state in database) netmanager = Cluster.get_network_manager() netmanager.clear_assigned_ips(instance) fire_callback_on_node_reset(instance) db().flush() @classmethod def update_cluster_assignment(cls, instance, cluster): """Update assignment of the node to the other cluster. This method primarily used by the cluster_upgrade extension for reassigning and reinstallation of a node. Be careful to use it outside of this extension because node still plugged to networks of a previous cluster. :param instance: An instance of :class:`Node`. :param cluster: An instance of :class:`Cluster`. """ roles = instance.roles instance.cluster_id = cluster.id instance.kernel_params = None instance.group_id = None instance.deployment_info = [] cls.update_roles(instance, []) cls.update_pending_roles(instance, roles) cls.remove_replaced_params(instance) cls.assign_group(instance) cls.set_network_template(instance) db().flush() @classmethod def update_by_agent(cls, instance, data): """Update Node instance with some specific cases for agent. * don't update provisioning or error state back to discover * don't update volume information if disks arrays is empty :param data: dictionary of key-value pairs as object fields :returns: Node instance """ # don't update provisioning and error back to discover data_status = data.get('status') if instance.status in ('provisioning', 'error'): if data.get('status', 'discover') == 'discover': logger.debug( u"Node {0} has provisioning or error status - " u"status not updated by agent".format( instance.human_readable_name ) ) data.pop('status', None) meta = data.get('meta', {}) # don't update volume information, if agent has sent an empty array if len(meta.get('disks', [])) == 0 and instance.meta.get('disks'): logger.warning( u'Node {0} has received an empty disks array - ' u'volume information will not be updated'.format( instance.human_readable_name ) ) meta['disks'] = instance.meta['disks'] # don't update volume information, if it is locked by node status if 'disks' in meta and cls.hardware_info_locked(instance): logger.debug("Volume information is locked for update on node %s", instance.human_readable_name) meta['disks'] = instance.meta['disks'] # (dshulyak) change this verification to NODE_STATUSES.deploying # after we will reuse ips from dhcp range if data.get('ip'): if instance.cluster_id: update_status = cls.check_ip_belongs_to_own_admin_network( instance, data['ip']) else: update_status = cls.check_ip_belongs_to_any_admin_network( instance, data['ip']) if update_status: if instance.status == consts.NODE_STATUSES.error and \ instance.error_type == consts.NODE_ERRORS.discover: # accept the status from agent if the node had wrong IP # previously if data_status: instance.status = data_status else: instance.status = consts.NODE_STATUSES.discover else: data.pop('status', None) return cls.update(instance, data) @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 " u"'{0}' which isn't added to cluster".format( instance.full_name)) return logger.debug( u"Updating roles for node {0}: {1}".format( instance.full_name, new_roles)) instance.roles = new_roles db().flush() @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 " u"'{0}' which isn't added to cluster".format( instance.full_name)) return logger.debug( u"Updating pending roles for node {0}: {1}".format( instance.full_name, new_pending_roles)) if new_pending_roles == []: # TODO(enchantner): research why the hell we need this Cluster.clear_pending_changes( instance.cluster, node_id=instance.id ) instance.pending_roles = new_pending_roles db().flush() @classmethod def update_primary_roles(cls, instance, new_primary_roles): """Update primary_roles for Node instance. Logs an error if node doesn't belong to Cluster :param instance: Node instance :param new_primary_roles: list of new pending role names :returns: None """ if not instance.cluster_id: logger.warning( u"Attempting to assign pending roles to node " u"'{0}' which isn't added to cluster".format( instance.full_name)) return assigned_roles = set(instance.roles + instance.pending_roles) for role in new_primary_roles: if role not in assigned_roles: logger.warning( u"Could not mark node {0} as primary for {1} role, " u"because there's no assigned {1} role.".format( instance.full_name, role) ) return logger.debug( u"Updating primary roles for node {0}: {1}".format( instance.full_name, new_primary_roles)) instance.primary_roles = new_primary_roles db().flush() @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 cls.assign_group(instance) network_manager = Cluster.get_network_manager(instance.cluster) network_manager.assign_networks_by_default(instance) cls.add_pending_change(instance, consts.CLUSTER_CHANGES.interfaces) cls.set_network_template(instance) @classmethod def set_network_template(cls, instance): template = instance.cluster.network_config.configuration_template cls.apply_network_template(instance, template) db().flush() @classmethod def add_pending_change(cls, instance, change): """Add pending change into Cluster. :param instance: Node instance :param change: string value of cluster change :returns: None """ if instance.cluster: Cluster.add_pending_changes( instance.cluster, change, node_id=instance.id ) @classmethod def get_admin_physical_iface(cls, instance): """Returns node's physical iface. In case if we have bonded admin iface, first of the bonded ifaces will be returned :param instance: Node instance :returns: interface instance """ admin_iface = Cluster.get_network_manager(instance.cluster) \ .get_admin_interface(instance) if admin_iface.type != consts.NETWORK_INTERFACE_TYPES.bond: return admin_iface for slave in admin_iface.slaves: if slave.pxe or slave.mac == instance.mac: return slave return admin_iface.slaves[-1] @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 :returns: None """ if instance.cluster: Cluster.clear_pending_changes( instance.cluster, node_id=instance.id ) netmanager = Cluster.get_network_manager( instance.cluster ) netmanager.clear_assigned_networks(instance) netmanager.clear_bond_configuration(instance) cls.update_roles(instance, []) cls.update_pending_roles(instance, []) cls.remove_replaced_params(instance) instance.cluster_id = None instance.group_id = None instance.kernel_params = None instance.primary_roles = [] instance.hostname = cls.default_slave_name(instance) from nailgun.objects import OpenstackConfig OpenstackConfig.disable_by_nodes([instance]) db().flush() db().refresh(instance) @classmethod def move_roles_to_pending_roles(cls, instance): """Move roles to pending_roles""" instance.pending_roles = instance.pending_roles + instance.roles instance.roles = [] instance.primary_roles = [] db().flush() @classmethod def get_slave_name(cls, instance): if not instance.hostname: return cls.default_slave_name(instance) return instance.hostname @classmethod def default_slave_name(cls, instance): return u"node-{node_id}".format(node_id=instance.id) @classmethod def generate_fqdn_by_hostname(cls, hostname): return u"{instance_name}.{dns_domain}" \ .format(instance_name=hostname, dns_domain=settings.DNS_DOMAIN) @classmethod def get_node_fqdn(cls, instance): return cls.generate_fqdn_by_hostname(instance.hostname) @classmethod def get_kernel_params(cls, instance): """Get kernel params Return cluster kernel_params if they weren't replaced by custom params. """ return (instance.kernel_params or Cluster.get_default_kernel_params(instance.cluster)) @classmethod def remove_replaced_params(cls, instance): instance.replaced_deployment_info = [] instance.replaced_provisioning_info = {} instance.network_template = None @classmethod def all_roles(cls, instance): roles = set(instance.roles + instance.pending_roles) roles -= set(instance.primary_roles) primary_roles = set([ 'primary-{0}'.format(role) for role in instance.primary_roles]) return sorted(roles | primary_roles) @classmethod def apply_network_template(cls, instance, template): if template is None: instance.network_template = None return template_body = template['adv_net_template'] # Get the correct nic_mapping for this node so we can # dynamically replace any interface references in any # template for this node. from nailgun.objects import NodeGroup node_group = NodeGroup.get_by_uid(instance.group_id).name if node_group not in template_body: node_group = 'default' node_name = cls.get_slave_name(instance) nic_mapping = template_body[node_group]['nic_mapping'] if node_name not in nic_mapping: node_name = 'default' nic_mapping = nic_mapping[node_name] # Replace interface references and re-parse JSON template_object = NetworkTemplate(jsonutils.dumps(template_body)) node_template = template_object.safe_substitute(nic_mapping) parsed_template = jsonutils.loads(node_template) output = parsed_template[node_group] output['templates'] = output.pop('network_scheme') output['roles'] = {} output['endpoints'] = {} for v in output['templates'].values(): for endpoint in v['endpoints']: output['endpoints'][endpoint] = {} for role, ep in v['roles'].items(): output['roles'][role] = ep instance.network_template = output db().flush() if instance.cluster: nm = Cluster.get_network_manager(instance.cluster) nm.assign_networks_by_template(instance) @classmethod def get_unique_hostname(cls, node, cluster_id): """Generate default hostname Hostname is 'node-{id}' if it's not used or 'node-{uuid} otherwise. It's needed for case when user have manually renamed any another node to 'node-{id}'. """ hostname = cls.get_slave_name(node) if cls.get_by_hostname(hostname, cluster_id): hostname = 'node-{0}'.format(node.uuid) return hostname @classmethod def get_nic_by_name(cls, instance, iface_name): nic = db().query(models.NodeNICInterface).filter_by( name=iface_name ).filter_by( node_id=instance.id ).first() return nic @classmethod def reset_vms_created_state(cls, node): if consts.VIRTUAL_NODE_TYPES.virt not in node.all_roles: return vms_conf = copy.deepcopy(node.attributes.vms_conf) for vm in vms_conf: vm['created'] = False node.attributes.vms_conf = vms_conf class NodeCollection(NailgunCollection): """Node collection""" #: Single Node object class single = Node @classmethod def eager_nodes_handlers(cls, iterable): """Eager load objects instances that is used in nodes handler. :param iterable: iterable (SQLAlchemy query) :returns: iterable (SQLAlchemy query) """ options = ( joinedload('cluster'), subqueryload_all('nic_interfaces.assigned_networks_list'), subqueryload_all('bond_interfaces.assigned_networks_list'), subqueryload_all('ip_addrs.network_data') ) return cls.eager_base(iterable, options) @classmethod def lock_nodes(cls, instances): """Locking nodes instances, fetched before, but required to be locked :param instances: list of nodes :return: list of locked nodes """ instances_ids = [instance.id for instance in instances] q = cls.filter_by_list(None, 'id', instances_ids, order_by='id') return cls.lock_for_update(q).all() @classmethod def get_by_group_id(cls, group_id): return cls.filter_by(None, group_id=group_id) @classmethod def get_by_ids(cls, ids): return db.query(models.Node).filter(models.Node.id.in_(ids)).all() @classmethod def reset_network_template(cls, instances): for instance in instances: instance.network_template = None @classmethod def delete_by_ids(cls, ids): fire_callback_on_node_collection_delete(ids) db.query(cls.single.model).filter( cls.single.model.id.in_(ids)).delete(synchronize_session=False) @classmethod def discovery_node_ids(self): """Ids of nodes which belong to the cluster and have 'discovery' status :returns: list of node ids """ q_discovery = db().query( models.Node.id).filter_by(status=consts.NODE_STATUSES.discover) return [_id for (_id,) in q_discovery]