<commit_before>r8_nested_require('node','meta')
module DTK
  class Node < Model
    set_relation_name(:node,:node)

    r8_nested_require('node','template')
    r8_nested_require('node','filter')
    r8_nested_require('node','clone')
    r8_nested_require('node','attribute')

    include CloneMixin
    extend NodeMetaClassMixin 
    extend AttributeClassMixin
    include AttributeMixin

    def self.common_columns()
      [
       :id,
       :group_id,
       :display_name,
       :name,
       :os_type,
       :type,
       :description,
       :status,
       :target_id,
       :ui,
       :external_ref,
       :admin_op_status
      ]
    end
#TODO: stub for feature_node_admin_state
    def persistent_hostname?()
#      true
      false
    end

    class Instance < self
     def self.component_list_fields()
       [:id,:display_name,:group_id,:external_ref,:ordered_component_ids]
     end

      def self.get(mh,opts={})
        sp_hash = {
          :cols => ([:id,:group_id,:display_name]+(opts[:cols]||[])).uniq,
          :filter => [:neq,:datacenter_datacenter_id,nil]
        }
        get_objs(mh,sp_hash)
      end

      def self.get_unique_instance_name(mh,display_name)
        display_name_regexp = Regexp.new("^#{display_name}")
        matches = get(mh,:cols=>[:display_name]).select{|r|r[:display_name] =~ display_name_regexp}
        if matches.empty?
          return display_name
        end
        index = 2
        matches.each do |r|
          instance_name = r[:display_name]
          if instance_name =~ /-([0-9]+$)/
            instance_index = $1.to_i
            if instance_index >= index
              index += 1
            end
          end
        end
        "#{display_name}-#{index.to_s}"
      end
    end


#TODO: end stub for feature_node_admin_state

    ### virtual column defs
    #######################
    #TODO: write as sql fn for efficiency
    def has_pending_change()
      ((get_field?(:action)||{})[:count]||0) > 0
    end

    def status()
      #assumes :is_deployed and :operational_status are set
      (not self[:is_deployed]) ? "staged" : self[:operational_status]
    end

    def target_id()
      get_field?(:datacenter_datacenter_id)
    end

    def name()
      get_field?(:display_name)
    end

    #######################
    # standard get methods
    def get_target(additional_columns = [])
      sp_hash = {
        :cols => [:id,:group_id,:display_name] + additional_columns,
        :filter => [:eq,:id,target_id()]
      }
      target = Model.get_obj(model_handle(:target),sp_hash)

      return target
    end

    def get_target_iaas_type()
      get_target().get_iaas_type()
    end

    def get_target_iaas_credentials()
      # TODO: Haris - When we support multiple IAAS we will need to modify logic here
      get_target().get_aws_compute_params()
    end

    def self.get_violations(id_handles)
      get_objs_in_set(id_handles,{:cols => [:violations]}).map{|r|r[:violation]}
    end

    def get_project()
      get_objects_col_from_sp_hash(:cols => [:project]).first
    end

    def self.get_ports(id_handles)
      get_objs_in_set(id_handles,{:cols => [:ports]},{:keep_ref_cols => true}).map{|r|r[:port]}
    end

    def get_ports(*types)
      port_list = self.class.get_ports([id_handle])
      i18n = get_i18n_mappings_for_models(:component,:attribute)
      port_list.map{|port|port.filter_and_process!(i18n,*types)}.compact
    end

    ######### Model apis

    def get_assembly?(cols=nil)
      if assembly_id = get_field?(:assembly_id)
        sp_hash = {
        :cols => cols||[:id,:group_id,:display_name],
          :filter => [:eq,:id,assembly_id]
        }
        Assembly::Instance.get_objs(model_handle(:assembly_instance),sp_hash).first
      end
    end

    def self.list(model_handle,opts={})
      target_filter = (opts[:target_idh] ? [:eq,:datacenter_datacenter_id,opts[:target_idh].get_id()] : [:neq,:datacenter_datacenter_id,nil])
      filter = [:and, [:oneof, :type, ["instance","staged"]], target_filter]
      sp_hash = {
        :cols => common_columns() + [:assemblies],
        :filter => filter
      }
      cols_except_name = common_columns() - [:display_name]
      get_objs(model_handle,sp_hash).map do |n|
        el = n.hash_subset(*cols_except_name)
        assembly_name = (n[:assembly]||{})[:display_name]
        el.merge(:display_name => user_friendly_name(n[:display_name],assembly_name))
      end.sort{|a,b|a[:display_name] <=> b[:display_name]}
    end

    def self.list_wo_assembly_nodes(model_handle)
      filter = [:and, [:oneof, :type, ["instance","staged"]], [:eq, :assembly_id, nil]]
      sp_hash = {
        :cols => common_columns() + [:assemblies],
        :filter => filter
      }
      cols_except_name = common_columns() - [:display_name]
      get_objs(model_handle,sp_hash).map do |n|
        el = n.hash_subset(*cols_except_name)
        assembly_name = (n[:assembly]||{})[:display_name]
        el.merge(:display_name => user_friendly_name(n[:display_name],assembly_name))
      end.sort{|a,b|a[:display_name] <=> b[:display_name]}
    end

    class << self
     private
      def user_friendly_name(node_name,assembly_name=nil)
        assembly_name ? "#{assembly_name}::#{node_name}" : node_name
      end
      #returns [node_name, assembly_name] later which could be null
      def parse_user_friendly_name(name)
        if name =~ Regexp.new("(^.+)#{AssemblyNodeNameSep}(.+$)")
          [$2,$1]
        else
          name
        end
      end
      AssemblyNodeNameSep = '::'
    end
    InfoCols = [:id,:display_name,:os_type,:type,:description,:status,:external_ref,:assembly_id]

    def info(opts={})
      ret = get_obj(:cols => InfoCols).hash_subset(*InfoCols)
      opts[:print_form] ? info_print_form_processing!(ret) : ret
    end

    def info_print_form_processing!(info_hash)
      if external_ref = info_hash[:external_ref]
        private_dns = external_ref[:private_dns_name]
        if private_dns.kind_of?(Hash)
          #then :private_dns_name is of form <public dns> => <private dns>
          external_ref[:private_dns_name] = private_dns.values.first 
        end
      end
      info_hash
    end

    
    def info_about(about,opts={})
      case about
       when :components
        get_objs(:cols => [:components],:keep_ref_cols => true).map do |r|
          r[:component].convert_to_print_form!()
        end.sort{|a,b|a[:display_name] <=> b[:display_name]}
       when :attributes
        get_attributes_print_form()
       else
        raise Error.new("TODO: not implemented yet: processing of info_about(#{about})")        
      end
    end

    def find_violations()
      cmps = get_objs(:cols => [:components],:keep_ref_cols => true)
      
      ret = Array.new
      return ret if cmps.empty?
      
      cmps.each do |cmp|
        sp_hash = {
          :cols => [:id, :type, :component_id, :service_id],
          :filter => [:eq, :id, cmp[:component][:module_branch_id]]
        }
        branch = Model.get_obj(model_handle(:module_branch),sp_hash)

        sp_cmp_hash = {
          :cols => [:id, :display_name, :dsl_parsed],
          :filter => [:eq, :id, branch[:component_id]]
        }
        cmp_module = Model.get_obj(model_handle(:component_module),sp_cmp_hash)
        
        ret << NodeViolations::NodeComponentParsingError.new(cmp_module[:display_name], "Component") unless cmp_module[:dsl_parsed]
      end

      ret
    end

    def add_component(component_template,component_title=nil)
      component_template.update_with_clone_info!()
      override_attrs = {:locked_sha => component_template.get_current_sha!()}
      if title_attr_name = check_and_ret_title_attribute_name?(component_template,component_title)
        component_type = component_template.get_field?(:component_type)
        override_attrs = {
          :ref => SQL::ColRef.cast(ComponentTitle.ref_with_title(component_type,component_title),:text),
          :display_name => SQL::ColRef.cast(ComponentTitle.display_name_with_title(component_type,component_title),:text)
        }
      end
      clone_opts = {:no_post_copy_hook => true,:ret_new_obj_with_cols => [:id,:display_name]}
      new_cmp = clone_into(component_template,override_attrs,clone_opts)
      new_cmp_idh = new_cmp.id_handle()
      if title_attr_name
        Component::Instance.set_title_attribute(new_cmp_idh,component_title,title_attr_name)
      end
      new_cmp_idh
    end

    def delete_component(component_idh)
      #first check that component_idh belongs to this instance
      sp_hash = {
        :cols => [:id, :display_name],
        :filter => [:and, [:eq, :id, component_idh.get_id()], [:eq, :node_node_id, id()]]
      }
      unless Model.get_obj(model_handle(:component),sp_hash)
        raise ErrorIdInvalid.new(component_idh.get_id(),:component)
      end
      Model.delete_instance(component_idh)
    end

    def self.check_valid_id(model_handle,id,assembly_id=nil)
      filter = 
        [:and,
         [:eq, :id, id],
         [:oneof, :type, ["instance","staged"]],
         [:neq, :datacenter_datacenter_id, nil],
         assembly_id && [:eq, :assembly_id, assembly_id]
        ].compact
      check_valid_id_helper(model_handle,id,filter)
    end

    def self.name_to_id(model_handle,name,assembly_id=nil)
      node_name, assembly_name = parse_user_friendly_name(name)
      assembly_id ||= assembly_name && Assembly::Instance.name_to_id(model_handle.createMH(:component),assembly_name)
      sp_hash =  {
        :cols => [:id,:assembly_id],
        :filter => [:and,
                    [:eq, :display_name, node_name],
                    [:oneof, :type, ["instance","staged"]],
                    [:neq, :datacenter_datacenter_id, nil],
                    [:eq, :assembly_id, assembly_id]]
      }
      name_to_id_helper(model_handle,name,sp_hash)
    end

    def update_external_ref_field(ext_ref_field,val)
      update({:external_ref => {ext_ref_field => val}},{:partial_value=>true})
    end

    def get_and_update_status!()
      #shortcut
      if has_key?(:is_deployed)
        return  "staged" if not self[:is_deployed]
      end
      update_obj!(:is_deployed,:external_ref,:operational_status)
      return  "staged" if not self[:is_deployed]
      get_and_update_operational_status!()
    end

    def get_and_update_operational_status!()
      update_obj!(:external_ref,:operational_status)
      op_status = CommandAndControl.get_node_operational_status(self)
      if op_status
        unless self[:operational_status] == op_status
          update_operational_status!(op_status)
        end
      end
      op_status || self[:operational_status]
    end

    def is_node_group?()
      ["node_group_instance"].include?(self[:type])
    end

    #attribute on node
    def update_operational_status!(op_status)
      update(:operational_status => op_status.to_s)
      self[:operational_status] = op_status.to_s
    end

    def update_admin_op_status!(op_status)
      update(:admin_op_status => op_status.to_s)
      self[:admin_op_status] = op_status.to_s
    end

    def update_agent_git_commit_id(agent_git_commit_id)
      update(:agent_git_commit_id => agent_git_commit_id)
      self[:agent_git_commit_id] = agent_git_commit_id      
    end

#TODO: these may be depracted
    def update_ordered_component_ids(order)
      ordered_component_ids = "{ :order => [#{order.join(',')}] }"
      update(:ordered_component_ids => ordered_component_ids)
      self[:ordered_component_ids] = ordered_component_ids      
    end

    def get_ordered_component_ids()
      ordered_component_ids = self[:ordered_component_ids]
      return Array.new unless ordered_component_ids
      eval(ordered_component_ids)[:order]
    end
#end of these may be depracted

    def get_external_ref()
      get_field?(:external_ref)||{}
    end

    def self.pbuilderid(node)
      node.get_external_ref()[:instance_id]
    end
    def pbuilderid()
      Node.pbuilderid(self)
    end

    def instance_id()
      get_external_ref()[:instance_id]
    end

    def persistent_dns()
      get_hostname_external_ref()[:persistent_dns]
    end

    def elastic_ip()
      get_hostname_external_ref()[:elastic_ip]
    end

    def get_hostname_external_ref()
      get_field?(:hostname_external_ref)||{}
    end
    private :get_hostname_external_ref

    
    #### related to distinguishing bewteen nodes and node groups

    def self.get_node_or_ng_summary(node_mh,node_ids)
      ret = Hash.new
      return ret if node_ids.empty?
      sp_hash = {
        :cols => [:id,:type,:node_or_ng_summary],
        :filter => [:oneof, :id, node_ids]
      }
      get_objs(node_mh,sp_hash).inject({}) do |ret,n|
        n.delete(:node_group_relation)
        node_member = n.delete(:node_member)
        node_id = n[:id]
        if n.is_node_group?()
          pntr = ret[node_id] ||= NodeGroup.create_as(n).merge(:node_group_members => Array.new)
          pntr[:node_group_members] << node_member if node_member
          ret
        else
          ret.merge(node_id => n)
        end
      end
    end

    def is_node?()
      NodeTypes.include?(get_field?(:type))
    end
    def is_node_group?()
      #short circuit
      return true if kind_of?(NodeGroup)
      NodeGroupTypes.include?(get_field?(:type))
    end
    NodeTypes = %w{instance image staged}
    NodeGroupTypes = %w{node_group_instance}

    #### end: related to distinguishing bewteen nodes and node groups

    def destroy_and_delete(opts={})
      update_obj!(:external_ref,:hostname_external_ref)
      if suceeeded = CommandAndControl.destroy_node?(self)
        delete_object(opts)
      end
      suceeeded
    end

    def delete_object(opts={})
      update_dangling_links()
      if opts[:update_task_template]
        unless assembly = opts[:assembly]
          raise Error.new("If update_task_template is assembled :assembly must be given as an option")
        end
        update_task_templates_when_deleted_node?(assembly)
      end
      Model.delete_instance(id_handle())
      true
    end

    def update_task_templates_when_deleted_node?(assembly)
      #TODO: can be more efficient if have Task::Template method that takes node and deletes all teh nodes component in bulk
      sp_hash = {
        #:only_one_per_node,:ref are put in for info needed when getting title
        :cols => [:id, :display_name, :node_node_id,:only_one_per_node,:ref],
        :filter => [:eq, :node_node_id, id()]
      }
      components = Component::Instance.get_objs(model_handle(:component),sp_hash)
      components.map{|cmp|Task::Template::ConfigComponents.update_when_deleted_component?(assembly,self,cmp)}
    end
    private :update_task_templates_when_deleted_node?

    def update_dangling_links()
      dangling_links_info_cmps = get_objs(:cols => [:dangling_input_links_from_components])
      dangling_links_info_nodes = get_objs(:cols => [:dangling_input_links_from_nodes])

      #TODO: if only processing external links, more efficeint to filter in sql query
      ndx_dangling_links_info = Hash.new
      (dangling_links_info_cmps + dangling_links_info_nodes).each do |r|
        link = r[:all_input_links]
        if link[:type] == "external"
          attr_id = link[:input_id]
          p = ndx_dangling_links_info[attr_id] ||= {:input_attribute => r[:input_attribute], :other_links => Array.new}
          new_el = {
            :attribute_link_id => link[:id], 
            :index_map => link[:index_map], 
          }
          if link[:id] == r[:attribute_link][:id]
            p[:deleted_link] = new_el
          else
            p[:other_links] << new_el
          end
        end
      end
      attr_mh = model_handle(:attribute)
      #update attribles connected to dangling links on input side
      updated_attrs = AttributeUpdateDerivedValues.update_for_delete_links(attr_mh,ndx_dangling_links_info.values)
      #add state changes for updated attributes and see if any connected attributes
      Attribute.propagate_and_optionally_add_state_changes(attr_mh,updated_attrs,:add_state_changes => true)
    end
    private :update_dangling_links


    def self.get_port_links(id_handles,*port_types)
      input_port_rows =  get_objs_in_set(id_handles,:columns => [:id, :display_name, :input_port_link_info]).select do |r|
        port_types.include?((r[:port]||{})[:type])
      end
      #TODO: implement using PortLink.common_columns and materialize
      input_port_rows.each do |r|
        r[:port_link][:ui] ||= {
          :type => R8::Config[:links][:default_type],
          :style => R8::Config[:links][:default_style]
        }<commit_msg>debugging change wrt to use of update_obj<commit_after>r8_nested_require('node','meta')
module DTK
  class Node < Model
    set_relation_name(:node,:node)

    r8_nested_require('node','template')
    r8_nested_require('node','filter')
    r8_nested_require('node','clone')
    r8_nested_require('node','attribute')

    include CloneMixin
    extend NodeMetaClassMixin 
    extend AttributeClassMixin
    include AttributeMixin

    def self.common_columns()
      [
       :id,
       :group_id,
       :display_name,
       :name,
       :os_type,
       :type,
       :description,
       :status,
       :target_id,
       :ui,
       :external_ref,
       :admin_op_status
      ]
    end
#TODO: stub for feature_node_admin_state
    def persistent_hostname?()
#      true
      false
    end

    class Instance < self
     def self.component_list_fields()
       [:id,:display_name,:group_id,:external_ref,:ordered_component_ids]
     end

      def self.get(mh,opts={})
        sp_hash = {
          :cols => ([:id,:group_id,:display_name]+(opts[:cols]||[])).uniq,
          :filter => [:neq,:datacenter_datacenter_id,nil]
        }
        get_objs(mh,sp_hash)
      end

      def self.get_unique_instance_name(mh,display_name)
        display_name_regexp = Regexp.new("^#{display_name}")
        matches = get(mh,:cols=>[:display_name]).select{|r|r[:display_name] =~ display_name_regexp}
        if matches.empty?
          return display_name
        end
        index = 2
        matches.each do |r|
          instance_name = r[:display_name]
          if instance_name =~ /-([0-9]+$)/
            instance_index = $1.to_i
            if instance_index >= index
              index += 1
            end
          end
        end
        "#{display_name}-#{index.to_s}"
      end
    end


#TODO: end stub for feature_node_admin_state

    ### virtual column defs
    #######################
    #TODO: write as sql fn for efficiency
    def has_pending_change()
      ((get_field?(:action)||{})[:count]||0) > 0
    end

    def status()
      #assumes :is_deployed and :operational_status are set
      (not self[:is_deployed]) ? "staged" : self[:operational_status]
    end

    def target_id()
      get_field?(:datacenter_datacenter_id)
    end

    def name()
      get_field?(:display_name)
    end

    #######################
    # standard get methods
    def get_target(additional_columns = [])
      sp_hash = {
        :cols => [:id,:group_id,:display_name] + additional_columns,
        :filter => [:eq,:id,target_id()]
      }
      target = Model.get_obj(model_handle(:target),sp_hash)

      return target
    end

    def get_target_iaas_type()
      get_target().get_iaas_type()
    end

    def get_target_iaas_credentials()
      # TODO: Haris - When we support multiple IAAS we will need to modify logic here
      get_target().get_aws_compute_params()
    end

    def self.get_violations(id_handles)
      get_objs_in_set(id_handles,{:cols => [:violations]}).map{|r|r[:violation]}
    end

    def get_project()
      get_objects_col_from_sp_hash(:cols => [:project]).first
    end

    def self.get_ports(id_handles)
      get_objs_in_set(id_handles,{:cols => [:ports]},{:keep_ref_cols => true}).map{|r|r[:port]}
    end

    def get_ports(*types)
      port_list = self.class.get_ports([id_handle])
      i18n = get_i18n_mappings_for_models(:component,:attribute)
      port_list.map{|port|port.filter_and_process!(i18n,*types)}.compact
    end

    ######### Model apis

    def get_assembly?(cols=nil)
      if assembly_id = get_field?(:assembly_id)
        sp_hash = {
        :cols => cols||[:id,:group_id,:display_name],
          :filter => [:eq,:id,assembly_id]
        }
        Assembly::Instance.get_objs(model_handle(:assembly_instance),sp_hash).first
      end
    end

    def self.list(model_handle,opts={})
      target_filter = (opts[:target_idh] ? [:eq,:datacenter_datacenter_id,opts[:target_idh].get_id()] : [:neq,:datacenter_datacenter_id,nil])
      filter = [:and, [:oneof, :type, ["instance","staged"]], target_filter]
      sp_hash = {
        :cols => common_columns() + [:assemblies],
        :filter => filter
      }
      cols_except_name = common_columns() - [:display_name]
      get_objs(model_handle,sp_hash).map do |n|
        el = n.hash_subset(*cols_except_name)
        assembly_name = (n[:assembly]||{})[:display_name]
        el.merge(:display_name => user_friendly_name(n[:display_name],assembly_name))
      end.sort{|a,b|a[:display_name] <=> b[:display_name]}
    end

    def self.list_wo_assembly_nodes(model_handle)
      filter = [:and, [:oneof, :type, ["instance","staged"]], [:eq, :assembly_id, nil]]
      sp_hash = {
        :cols => common_columns() + [:assemblies],
        :filter => filter
      }
      cols_except_name = common_columns() - [:display_name]
      get_objs(model_handle,sp_hash).map do |n|
        el = n.hash_subset(*cols_except_name)
        assembly_name = (n[:assembly]||{})[:display_name]
        el.merge(:display_name => user_friendly_name(n[:display_name],assembly_name))
      end.sort{|a,b|a[:display_name] <=> b[:display_name]}
    end

    class << self
     private
      def user_friendly_name(node_name,assembly_name=nil)
        assembly_name ? "#{assembly_name}::#{node_name}" : node_name
      end
      #returns [node_name, assembly_name] later which could be null
      def parse_user_friendly_name(name)
        if name =~ Regexp.new("(^.+)#{AssemblyNodeNameSep}(.+$)")
          [$2,$1]
        else
          name
        end
      end
      AssemblyNodeNameSep = '::'
    end
    InfoCols = [:id,:display_name,:os_type,:type,:description,:status,:external_ref,:assembly_id]

    def info(opts={})
      ret = get_obj(:cols => InfoCols).hash_subset(*InfoCols)
      opts[:print_form] ? info_print_form_processing!(ret) : ret
    end

    def info_print_form_processing!(info_hash)
      if external_ref = info_hash[:external_ref]
        private_dns = external_ref[:private_dns_name]
        if private_dns.kind_of?(Hash)
          #then :private_dns_name is of form <public dns> => <private dns>
          external_ref[:private_dns_name] = private_dns.values.first 
        end
      end
      info_hash
    end

    
    def info_about(about,opts={})
      case about
       when :components
        get_objs(:cols => [:components],:keep_ref_cols => true).map do |r|
          r[:component].convert_to_print_form!()
        end.sort{|a,b|a[:display_name] <=> b[:display_name]}
       when :attributes
        get_attributes_print_form()
       else
        raise Error.new("TODO: not implemented yet: processing of info_about(#{about})")        
      end
    end

    def find_violations()
      cmps = get_objs(:cols => [:components],:keep_ref_cols => true)
      
      ret = Array.new
      return ret if cmps.empty?
      
      cmps.each do |cmp|
        sp_hash = {
          :cols => [:id, :type, :component_id, :service_id],
          :filter => [:eq, :id, cmp[:component][:module_branch_id]]
        }
        branch = Model.get_obj(model_handle(:module_branch),sp_hash)

        sp_cmp_hash = {
          :cols => [:id, :display_name, :dsl_parsed],
          :filter => [:eq, :id, branch[:component_id]]
        }
        cmp_module = Model.get_obj(model_handle(:component_module),sp_cmp_hash)
        
        ret << NodeViolations::NodeComponentParsingError.new(cmp_module[:display_name], "Component") unless cmp_module[:dsl_parsed]
      end

      ret
    end

    def add_component(component_template,component_title=nil)
      component_template.update_with_clone_info!()
      override_attrs = {:locked_sha => component_template.get_current_sha!()}
      if title_attr_name = check_and_ret_title_attribute_name?(component_template,component_title)
        component_type = component_template.get_field?(:component_type)
        override_attrs = {
          :ref => SQL::ColRef.cast(ComponentTitle.ref_with_title(component_type,component_title),:text),
          :display_name => SQL::ColRef.cast(ComponentTitle.display_name_with_title(component_type,component_title),:text)
        }
      end
      clone_opts = {:no_post_copy_hook => true,:ret_new_obj_with_cols => [:id,:display_name]}
      new_cmp = clone_into(component_template,override_attrs,clone_opts)
      new_cmp_idh = new_cmp.id_handle()
      if title_attr_name
        Component::Instance.set_title_attribute(new_cmp_idh,component_title,title_attr_name)
      end
      new_cmp_idh
    end

    def delete_component(component_idh)
      #first check that component_idh belongs to this instance
      sp_hash = {
        :cols => [:id, :display_name],
        :filter => [:and, [:eq, :id, component_idh.get_id()], [:eq, :node_node_id, id()]]
      }
      unless Model.get_obj(model_handle(:component),sp_hash)
        raise ErrorIdInvalid.new(component_idh.get_id(),:component)
      end
      Model.delete_instance(component_idh)
    end

    def self.check_valid_id(model_handle,id,assembly_id=nil)
      filter = 
        [:and,
         [:eq, :id, id],
         [:oneof, :type, ["instance","staged"]],
         [:neq, :datacenter_datacenter_id, nil],
         assembly_id && [:eq, :assembly_id, assembly_id]
        ].compact
      check_valid_id_helper(model_handle,id,filter)
    end

    def self.name_to_id(model_handle,name,assembly_id=nil)
      node_name, assembly_name = parse_user_friendly_name(name)
      assembly_id ||= assembly_name && Assembly::Instance.name_to_id(model_handle.createMH(:component),assembly_name)
      sp_hash =  {
        :cols => [:id,:assembly_id],
        :filter => [:and,
                    [:eq, :display_name, node_name],
                    [:oneof, :type, ["instance","staged"]],
                    [:neq, :datacenter_datacenter_id, nil],
                    [:eq, :assembly_id, assembly_id]]
      }
      name_to_id_helper(model_handle,name,sp_hash)
    end

    def update_external_ref_field(ext_ref_field,val)
      update({:external_ref => {ext_ref_field => val}},{:partial_value=>true})
    end

    def get_and_update_status!()
      #shortcut
      if has_key?(:is_deployed)
        return  "staged" if not self[:is_deployed]
      end
      update_obj!(:is_deployed,:external_ref,:operational_status)
      return  "staged" if not self[:is_deployed]
      get_and_update_operational_status!()
    end

    def get_and_update_operational_status!()
      update_obj!(:external_ref,:operational_status)
      op_status = CommandAndControl.get_node_operational_status(self)
      if op_status
        unless self[:operational_status] == op_status
          update_operational_status!(op_status)
        end
      end
      op_status || self[:operational_status]
    end

    def is_node_group?()
      ["node_group_instance"].include?(self[:type])
    end

    #attribute on node
    def update_operational_status!(op_status)
      update(:operational_status => op_status.to_s)
      self[:operational_status] = op_status.to_s
    end

    def update_admin_op_status!(op_status)
      update(:admin_op_status => op_status.to_s)
      self[:admin_op_status] = op_status.to_s
    end

    def update_agent_git_commit_id(agent_git_commit_id)
      update(:agent_git_commit_id => agent_git_commit_id)
      self[:agent_git_commit_id] = agent_git_commit_id      
    end

#TODO: these may be depracted
    def update_ordered_component_ids(order)
      ordered_component_ids = "{ :order => [#{order.join(',')}] }"
      update(:ordered_component_ids => ordered_component_ids)
      self[:ordered_component_ids] = ordered_component_ids      
    end

    def get_ordered_component_ids()
      ordered_component_ids = self[:ordered_component_ids]
      return Array.new unless ordered_component_ids
      eval(ordered_component_ids)[:order]
    end
#end of these may be depracted

    def get_external_ref()
      get_field?(:external_ref)||{}
    end

    def self.pbuilderid(node)
      node.get_external_ref()[:instance_id]
    end
    def pbuilderid()
      Node.pbuilderid(self)
    end

    def instance_id()
      get_external_ref()[:instance_id]
    end

    def persistent_dns()
      get_hostname_external_ref()[:persistent_dns]
    end

    def elastic_ip()
      get_hostname_external_ref()[:elastic_ip]
    end

    def get_hostname_external_ref()
      get_field?(:hostname_external_ref)||{}
    end
    private :get_hostname_external_ref

    
    #### related to distinguishing bewteen nodes and node groups

    def self.get_node_or_ng_summary(node_mh,node_ids)
      ret = Hash.new
      return ret if node_ids.empty?
      sp_hash = {
        :cols => [:id,:type,:node_or_ng_summary],
        :filter => [:oneof, :id, node_ids]
      }
      get_objs(node_mh,sp_hash).inject({}) do |ret,n|
        n.delete(:node_group_relation)
        node_member = n.delete(:node_member)
        node_id = n[:id]
        if n.is_node_group?()
          pntr = ret[node_id] ||= NodeGroup.create_as(n).merge(:node_group_members => Array.new)
          pntr[:node_group_members] << node_member if node_member
          ret
        else
          ret.merge(node_id => n)
        end
      end
    end

    def is_node?()
      NodeTypes.include?(get_field?(:type))
    end
    def is_node_group?()
      #short circuit
      return true if kind_of?(NodeGroup)
      NodeGroupTypes.include?(get_field?(:type))
    end
    NodeTypes = %w{instance image staged}
    NodeGroupTypes = %w{node_group_instance}

    #### end: related to distinguishing bewteen nodes and node groups

    def destroy_and_delete(opts={})
      update_obj!(:external_ref,:hostname_external_ref)
      if suceeeded = CommandAndControl.destroy_node?(self)
        delete_object(opts)
      end
      suceeeded
    end

    def delete_object(opts={})
      update_dangling_links()
      if opts[:update_task_template]
        unless assembly = opts[:assembly]
          raise Error.new("If update_task_template is assembled :assembly must be given as an option")
        end
        update_task_templates_when_deleted_node?(assembly)
      end
      Model.delete_instance(id_handle())
      true
    end

    def update_task_templates_when_deleted_node?(assembly)
      #TODO: can be more efficient if have Task::Template method that takes node and deletes all teh nodes component in bulk
      sp_hash = {
        #:only_one_per_node,:ref are put in for info needed when getting title
        :cols => [:id, :display_name, :node_node_id,:only_one_per_node,:ref],
        :filter => [:eq, :node_node_id, id()]
      }
      components = Component::Instance.get_objs(model_handle(:component),sp_hash)
      components.map{|cmp|Task::Template::ConfigComponents.update_when_deleted_component?(assembly,self,cmp)}
    end
    private :update_task_templates_when_deleted_node?

    def update_dangling_links()
      dangling_links_info_cmps = get_objs(:cols => [:dangling_input_links_from_components])
      dangling_links_info_nodes = get_objs(:cols => [:dangling_input_links_from_nodes])

      #TODO: if only processing external links, more efficeint to filter in sql query
      ndx_dangling_links_info = Hash.new
      (dangling_links_info_cmps + dangling_links_info_nodes).each do |r|
        link = r[:all_input_links]
        if link[:type] == "external"
          attr_id = link[:input_id]
          p = ndx_dangling_links_info[attr_id] ||= {:input_attribute => r[:input_attribute], :other_links => Array.new}
          new_el = {
            :attribute_link_id => link[:id], 
            :index_map => link[:index_map], 
          }
          if link[:id] == r[:attribute_link][:id]
            p[:deleted_link] = new_el
          else
            p[:other_links] << new_el
          end
        end
      end
      attr_mh = model_handle_with_auth_info(:attribute)
      #update attributes connected to dangling links on input side
      updated_attrs = AttributeUpdateDerivedValues.update_for_delete_links(attr_mh,ndx_dangling_links_info.values)
      #add state changes for updated attributes and see if any connected attributes
      Attribute.propagate_and_optionally_add_state_changes(attr_mh,updated_attrs,:add_state_changes => true)
    end
    private :update_dangling_links


    def self.get_port_links(id_handles,*port_types)
      input_port_rows =  get_objs_in_set(id_handles,:columns => [:id, :display_name, :input_port_link_info]).select do |r|
        port_types.include?((r[:port]||{})[:type])
      end
      #TODO: implement using PortLink.common_columns and materialize
      input_port_rows.each do |r|
        r[:port_link][:ui] ||= {
          :type => R8::Config[:links][:default_type],
          :style => R8::Config[:links][:default_style]
        }
{'fraction_non_alphanumeric': 0.11929298981426004, 'fraction_numerical': 0.0013181545835829838, 'mean_word_length': 3.056507473569085, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 137, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6057048', 'n_tokens_mistral': 11089, 'n_tokens_neox': 10866, 'n_words': 1850}