class/subclass relationship ordering and containment

922 views
Skip to first unread message

Mike Reed

unread,
Sep 19, 2014, 7:01:28 PM9/19/14
to puppet...@googlegroups.com
Hello all,

I have a question about class and subclass relationships and what is/isn't the ideal way to go about such a thing.  Please bear with me  as I'm still refining my understanding of containment. Let's say I have a puppet module which manages the install of puppet and has the following pieces (currently using puppet v.3.4.3):

init.pp
class puppet {
  # evaluate supporting classes
  include puppet::params
  include puppet::config
  include puppet::service
 
  anchor { 'puppet::begin' : } ->
  class { '::puppet::params' : } ->
  class { '::puppet::config' : } ~>
  class { '::puppet::service' : } ->
  anchor { 'puppet::end' : }
}

params.pp
class puppet::params {
# puppet general params
  $puppet_path            = '/etc/puppet'
  $puppet_config_template = 'puppet/puppet.conf.erb'
  $puppet_package         = 'puppet'
  $puppet_common_package  = 'puppet-common'
  $puppet_service_ensure  = 'running'
  $puppet_service_enable  = true
  $puppet_prod_version    = '3.6.2-1puppetlabs1'
  $puppet_dev_version     = '3.6.2-1puppetlabs1'
  validate_string($puppet_path)
  validate_string($puppet_config_template)
  validate_string($puppet_package)
  validate_string($puppet_common_package)
  validate_string($puppet_service_ensure)
  validate_bool($puppet_service_enable)
  validate_string($puppet_prod_version)
  validate_string($puppet_dev_version)
}

config.pp
class puppet::config (

  $puppet_config_path     = $::puppet::params::puppet_config_path,
  $puppet_config_template = $::puppet::params::puppet_config_template,
  $puppet_service         = $::puppet::params::puppet_service,
 
) inherits puppet::params {

  file { 'puppet.conf' :
    ensure  => present,
    path    => "${puppet_config_path}/",
    content => template("${puppet_config_template}"),
    notify  => Service["${puppet_service}"],
  }
}

service.pp

class puppet::service (

  $puppet_package         = $::puppet::params::puppet_package,
**truncated variables for sake of a long post**

) inherits puppet::config {

  package { "${puppet_package}":
    ensure  => "${puppet_prod_version}",
  }

  package { "${puppet_common_package}":
    ensure  => "${puppet_prod_version}",
  }

  service { "${puppet_service}":
    ensure     => "${puppet_service_ensure}",
    name       => "${puppet_service}",
    enable     => "${puppet_service_enable}",
    hasrestart => true,
    hasstatus  => true,
    subscribe  => Package["${puppet_config_template}"],
  }
}

Based on the above, I've left a few things which I feel don't belong but for the sake of my questions, they're included.

Per the above init.pp, I've added an anchor to force ordering.  My understanding is that this has nothing to do with application-order and more to do with parse-order.  With that said, I have a few questions:

1.  By adding the 'include' in init.pp, my understanding is that simply says to 'evaluate' the subclasses but does not indicate an order to which subclasses are to be applied.  Is that correct?

2.  I think the 'inherits' function is depreciated but should each instance be replaced with a 'contain' (based on the order I want) throughout my subclass manifests?  My understanding is that I should never 'contain' more than one subclass within the same module as puppet will be confused on ordering.

3.  I rather like the idea of the anchor in the init.pp because I only have one place to go to, in order to see the relationship of the subclasses. With the introduction of the 'contain' feature, I feel like the anchor is no longer needed; however, is there a preferred way of ordering these subclasses without using the anchor pattern?

As always, thanks in advance for your help.

Cheers,

Mike

 


Nan Liu

unread,
Sep 19, 2014, 8:37:57 PM9/19/14
to puppet...@googlegroups.com
TLDR summary:

1. include/require class does not provide containment.
2. anchor is just a empty resource for containment.
3. contain class provides containment without the need for anchor
4. contain may cause unnecessary relationship, so use it only when appropriate
5. The original purpose for inherits, resource parameter override, is rarely used. These days it's mostly a hack to have module data. classes inherit params class which only contain puppet variables and no resources. (I won't get into this and only focus on the previous points)

validate don't serve any purpose here, since you setting these values to a specific value.
The anchor is not parse-order, it to server as a ghost containment resource for the class. The syntax short hand is what confusing. You can totally rewrite as following:

  class { '::puppet::params' : require => Anchor['puppet::begin']}
  class { '::puppet::config' : } ~>
  class { '::puppet::service' : before => Anchor['puppet::end']}
  anchor { ['puppet::begin', 'puppet::end']: }
 
1.  By adding the 'include' in init.pp, my understanding is that simply says to 'evaluate' the subclasses but does not indicate an order to which subclasses are to be applied.  Is that correct?

Mostly.
 
2.  I think the 'inherits' function is depreciated but should each instance be replaced with a 'contain' (based on the order I want) throughout my subclass manifests?  My understanding is that I should never 'contain' more than one subclass within the same module as puppet will be confused on ordering.

The inherits params class data pattern can't be replaced by contain. contain is a new function that is used to eliminate the need for anchor.

3.  I rather like the idea of the anchor in the init.pp because I only have one place to go to, in order to see the relationship of the subclasses. With the introduction of the 'contain' feature, I feel like the anchor is no longer needed; however, is there a preferred way of ordering these subclasses without using the anchor pattern?

Anchor is not intended to order subclass, it's suppose to help other class calling this class to have containment. If you want your internal class to have order just do:

  class { '::puppet::params' : } ->
  class { '::puppet::config' : } ~>
  class { '::puppet::service' :}

If the calling class use 'contain puppet' instead of 'include puppet' you don't need the anchors.

Unfortunately this is a confusing subject. I'll provide some examples and step through them:

1. include/require class does not provide containment for classes with no resource.

You would expect the following code to work:

class software {
  include 'software::install'
  include 'software::config'
  include 'software::service'
  Class['software::install'] -> Class['software::config'] ~> Class['software::service']
}

node example{
  include('repo', 'software', 'postinstall')
  Class['repo'] -> Class['Software'] -> Class['postinstall']
}

Unfortunately, because software does not have any resource, Puppet does not translate the relationship to:
Class['repo'] -> Class['software::install'] -> Class['software::config'] ~> Class['software::service'] -> Class['postinstall']

This is because include doesn't offer containment around classes (that nest other classes). Puppet in this cause behaves like:
Class['repo'] -> Class['postinstall']
It still enforces the following, but not in any relation with the other class:
Class['software::install'] -> Class['software::config'] ~> Class['software::service'] 

2. anchor is just a empty resource for containment.

Let's change the original software class:
class software {
  include 'software::install'
  include 'software::config'
  include 'software::service'
  anchor { ['software::start', 'software::end']: }
  Anchor['software::start'] -> Class['software::install'] -> Class['software::config'] ~> Class['software::service'] ->Anchor[''software::end']
}

node example {
  include('repo', 'software', 'postinstall')
  Class['repo'] -> Class['Software'] -> Class['postinstall']
}

Now the it behaves as expected with anchor resources:
Class['repo'] -> Anchor['software::start'] -> Class['software::install'] -> Class['software::config'] ~> Class['software::service'] -> Anchor[''software::end'] -> Class['postinstall']

3. contain class provides containment without the need for anchor.

class software {
  include 'software::install'
  include 'software::config'
  include 'software::service'
  Class['software::install'] -> Class['software::config'] ~> Class['software::service']
}

node example {
  contain('repo', 'software', 'postinstall')
  Class['repo'] -> Class['Software'] -> Class['postinstall']
}

Now puppet will translate this to:
Class['repo'] -> Class['software::install'] -> Class['software::config'] ~> Class[''software::service'] -> Class['postinstall']

In this case containment avoided the need for anchor resources.

4. However use containment cautiously since it may introduce unnecessary relationship.

class software {
  contain repo
}

behaves the same as:
class software {
  include repo
  anchor { ['software::start', 'software::end']: }
  Anchor[software::start] -> Class['repo'] -> Anchor['software::end']
}

Here's a simple example why you shouldn't use contain instead of include carelessly:

class repo {  }

class software_a {
  contain repo
}

class software_b {
  contain repo
}

include software_a, software_b
Class[software_a] -> Class[software_b]

I'll leave it as an exercise for you to work out how to get the final example working. 

HTH,

Nan

jcbollinger

unread,
Sep 22, 2014, 10:14:33 AM9/22/14
to puppet...@googlegroups.com


I re-emphasize that point.  The Anchor pattern has NOTHING to do with parse / evaluation order, exactly contrary to the OP's assertion.  It is only relevant to client-side order of resource application.

 
The syntax short hand is what confusing. You can totally rewrite as following:

  class { '::puppet::params' : require => Anchor['puppet::begin']}
  class { '::puppet::config' : } ~>
  class { '::puppet::service' : before => Anchor['puppet::end']}
  anchor { ['puppet::begin', 'puppet::end']: }
 


Although that does have exactly the same application-order implications as the OP's version, it has slightly different evaluation / catalog-building semantics, owing to the use of resource-like class declarations in place of 'include' / 'require' / 'contain'.  That probably does not matter for this example, but it might matter in some other cases.

 
1.  By adding the 'include' in init.pp, my understanding is that simply says to 'evaluate' the subclasses but does not indicate an order to which subclasses are to be applied.  Is that correct?

Mostly.
 
2.  I think the 'inherits' function is depreciated but should each instance be replaced with a 'contain' (based on the order I want) throughout my subclass manifests?  My understanding is that I should never 'contain' more than one subclass within the same module as puppet will be confused on ordering.

The inherits params class data pattern can't be replaced by contain. contain is a new function that is used to eliminate the need for anchor.


The inherits params class data pattern cannot be replaced by contain / require / include because its purpose it to ensure that the parent class is evaluated before any part of the child class, and before the child class's parameter list in particular.  This exploits a side effect of class inheritance -- it is not the real purpose of class inheritance (which purpose is little used any more).  If you don't need the variables of the params class for class parameter defaults, however, then you can replace inheritance with 'include'ing (or 'require'ing or 'contain'ing) the params class it the start of erstwhile child class's body.

And specifically to the OP's point, no, Puppet will not be confused by one class 'contain'ing multiple classes.  It might, however, be confused by multiple classes all 'contain'ing the same class.  That confusion would likely manifest in the form of an error complaining about a relationship cycle.

 

3.  I rather like the idea of the anchor in the init.pp because I only have one place to go to, in order to see the relationship of the subclasses. With the introduction of the 'contain' feature, I feel like the anchor is no longer needed; however, is there a preferred way of ordering these subclasses without using the anchor pattern?

Anchor is not intended to order subclass, it's suppose to help other class calling this class to have containment.



That's an odd way to phrase it.  I'd put it like this: the purpose of the Anchor pattern and of 'contain' is to allow the class employing it to provide containment of the classes it declares.  I add also that such containment is only sometimes needed or wanted.

 
If you want your internal class to have order just do:


  class { '::puppet::params' : } ->
  class { '::puppet::config' : } ~>
  class { '::puppet::service' :}



Or alternatively, if you declare the classes via 'contain' then you can specify order of application via resource references:

contain '::puppet::params'
contain '::puppet::config'
contain '::puppet::service'

Class['::puppet::params'] ->
Class['::puppet::config'] ~>
Class['::puppet::service']

 
If the calling class use 'contain puppet' instead of 'include puppet' you don't need the anchors.

Unfortunately this is a confusing subject. I'll provide some examples and step through them:

1. include/require class does not provide containment for classes with no resource.

You would expect the following code to work:

class software {
  include 'software::install'
  include 'software::config'
  include 'software::service'
  Class['software::install'] -> Class['software::config'] ~> Class['software::service']
}

node example{
  include('repo', 'software', 'postinstall')
  Class['repo'] -> Class['Software'] -> Class['postinstall']
}

Unfortunately, because software does not have any resource,
 
Puppet does not translate the relationship to:
Class['repo'] -> Class['software::install'] -> Class['software::config'] ~> Class['software::service'] -> Class['postinstall']


For the record, whether Class['software'] declares any resources is not (directly) a factor there.  Relationships between classes are effectively forwarded to the ordinary resources declared by those classes, but not to other classes declared by those classes.  We say that classes "contain" the ordinary resources they declare, but not the classes they declare (other than those they declare via the 'contain' function).  That's useful and intentional, but sometimes you want classes to also contain the other classes they declare.

 

This is because include doesn't offer containment around classes (that nest other classes). Puppet in this cause behaves like:
Class['repo'] -> Class['postinstall']
It still enforces the following, but not in any relation with the other class:
Class['software::install'] -> Class['software::config'] ~> Class['software::service'] 


 
3. contain class provides containment without the need for anchor.

class software {
  include 'software::install'
  include 'software::config'
  include 'software::service'


I think Nan meant to use 'contain' instead of 'include' in the three lines above.  If not, then it's still what he should have done :-)

 
  Class['software::install'] -> Class['software::config'] ~> Class['software::service']
}

node example {
  contain('repo', 'software', 'postinstall')


I think that's a little confused.  I'm not sure whether 'contain' would work when called directly from a node block, but if it did then the only reasonable result (causing the specified classes to be contained by the node) would be useless, because there is no way to express ordering constraints involving whole node blocks.

 
  Class['repo'] -> Class['Software'] -> Class['postinstall']
}

Now puppet will translate this to:
Class['repo'] -> Class['software::install'] -> Class['software::config'] ~> Class[''software::service'] -> Class['postinstall']

In this case containment avoided the need for anchor resources.


With class 'software' rather than the node block doing the containing, yes, that would be the result.

 

4. However use containment cautiously since it may introduce unnecessary relationship.



Yes, but not any relationships inconsistent with those produced by applying the Anchor pattern.  You can replace the Anchor pattern with 'contain'ing the erstwhile anchored classes without risk of creating (new) relationship cycles.  That may nevertheless produce more relationships overall, however, which incrementally increases Puppet's work.  I think, though, that Nan's point was actually that it is not necessarily safe for just any class to 'contain' any other class.  Sometimes non-containment semantics are exactly what you want.

 
class software {
  contain repo
}

behaves the same as:
class software {
  include repo
  anchor { ['software::start', 'software::end']: }
  Anchor[software::start] -> Class['repo'] -> Anchor['software::end']
}

Here's a simple example why you shouldn't use contain instead of include carelessly:

class repo {  }

class software_a {
  contain repo
}

class software_b {
  contain repo
}

include software_a, software_b
Class[software_a] -> Class[software_b]

I'll leave it as an exercise for you to work out how to get the final example working. 



I won't give away the solution to the exercise, but the take-home point is that you should use 'contain' (or the Anchor pattern) only where you really mean that the contained class is, for all intents and purposes, part of the containing class.  Where you do mean that, do use 'contain'.


John

Mike Reed

unread,
Sep 22, 2014, 12:18:55 PM9/22/14
to puppet...@googlegroups.com
Hey Nan and John,

Thank you both for the replies and insight into my questions below; they are most helpful and very much appreciated.

Based on your answers, I have few other questions that occurred to me:

In response to my question about inherits, John had mentioned this below: "If you don't need the variables of the params class for class parameter defaults, however, then you can replace inheritance with 'include'ing (or 'require'ing or 'contain'ing) the params class it the start of erstwhile child class's body."

In my example config.pp, I'm using things like "$puppet_config_path = $::puppet::params::puppet_config_path," which obviously points to my params.pp file and thus would in theory, pull the variable value and evaluate accordingly.  If I am indeed pointing these local variables to my params.pp, it doesn't seem obvious that I need any include or inhert to params.pp in my subclasses.  In this example, I do indeed need the variables from my params class for my subclass defaults but because I'm specifically calling these with the use of local variables, is it advisable to still "include" the params.pp subclass?  It seems that my manifests would be more tidy if I didn't have include/inherits littered throughout my subclasses.

Further, if I had a selector or case statement in my params.pp, they it would seem appropriate to "include" params.pp in my config.pp because I indeed do need that selector/case statement to be evaluated before the config.pp subclass can be completed.  Am I thinking about this correctly?

Lastly, it seems that in the case of any module with subclasses, I want and need to include class/subclass ordering or the sake of ordering sanity.  In your opinion, should I be including subclass ordering in my init.pp for each module I write (assuming it includes subclasses)? 

Thank you guys for your invaluable information and for you patience.

Cheers,

Mike

jcbollinger

unread,
Sep 23, 2014, 10:36:13 AM9/23/14
to puppet...@googlegroups.com


On Monday, September 22, 2014 11:18:55 AM UTC-5, Mike Reed wrote:
Hey Nan and John,

Thank you both for the replies and insight into my questions below; they are most helpful and very much appreciated.

Based on your answers, I have few other questions that occurred to me:

In response to my question about inherits, John had mentioned this below: "If you don't need the variables of the params class for class parameter defaults, however, then you can replace inheritance with 'include'ing (or 'require'ing or 'contain'ing) the params class it the start of erstwhile child class's body."

In my example config.pp, I'm using things like "$puppet_config_path = $::puppet::params::puppet_config_path," which obviously points to my params.pp file and thus would in theory, pull the variable value and evaluate accordingly.


I'm not quite sure what you mean by that.  It sounds like you may have the misapprehension that upon encountering a reference to a class variable, Puppet ensures that the variable's class has already been evaluated.  That is not the case.  The future evaluator apparently catches cases where the variable's host class has not been evaluated yet, but it is not safe for it to autodeclare the class at that point.

 
  If I am indeed pointing these local variables to my params.pp, it doesn't seem obvious that I need any include or inhert to params.pp in my subclasses.  In this example, I do indeed need the variables from my params class for my subclass defaults but because I'm specifically calling these with the use of local variables, is it advisable to still "include" the params.pp subclass?  It seems that my manifests would be more tidy if I didn't have include/inherits littered throughout my subclasses.


Why do you think the use of which you put the other-class variables has anything to do with the requirements for accessing them in the first place?  Your classes puppet::config and puppet::service should inherit from puppet::params because they use its variables for their own parameter defaults; that way they are better self-contained.  If they do not do so then they make every one of their users responsible for ensuring that puppet::params is evaluated first.  If these classes are intended to be private to your module then you can make that work, but it's still worse form.

 

Further, if I had a selector or case statement in my params.pp, they it would seem appropriate to "include" params.pp in my config.pp because I indeed do need that selector/case statement to be evaluated before the config.pp subclass can be completed.  Am I thinking about this correctly?



Yes and no.  The presence specifically of a selector or case statement in puppet::params doesn't really factor in.  You use that class's variables, and you need the class itself to have been evaluated before you do.  Again, that is the point of inheriting from it in this context.  Declaring it via "include" etc. (or via a resource-like class declaration) would also have that effect, but would not ensure puppet::params is evaluated soon enough for its variables to be used as class parameter defaults.  In your case, it is actually sufficient for class puppet to 'include' puppet::params before puppet::config and puppet:service, provided that the latter two are never declared by anyone else, but if you are going to use the params class pattern then you ought to do it right.

Understand, too, that 'include' et. al. are declarations of the specified class(es).  The statement "include 'foo'" has very nearly the same meaning as "class { 'foo': }".  It tells Puppet that class 'foo' must be included in the catalog, not in the current manifest.  Any given class will be evaluated at most once, so "include 'foo'" is a no-op if class "foo" has already been evaluated (this is a very valuable feature).  Its 'require' and 'contain' brethren always have their effects on class relationships, but their class-evaluation aspect is also a no-op for classes that have already been evaluated.

Also, do not confuse resource-like class declarations with resource references to declared classes.  You can apply chain operators to both, even mixed, but you will cause yourself grief if you use a declaration where you really wanted a reference.  Your class 'puppet' suffers from that issue.

 
Lastly, it seems that in the case of any module with subclasses, I want and need to include class/subclass ordering or the sake of ordering sanity.  In your opinion, should I be including subclass ordering in my init.pp for each module I write (assuming it includes subclasses)? 



You should declare class relationships where there are genuine order-of-application constraints.  You should avoid them otherwise.  The nature of the classes is irrelevant.

I would rewrite your class 'puppet' like so:

init.pp
----
class puppet {
  # Managing puppet consists of first managing its config,
  # then managing the Puppet service.

  contain '::puppet::config'
  contain '::puppet::service'
  Class['::puppet::config'] -> Class['::puppet::service']
}


From what I can see, your classes puppet::config and puppet::service are already just as they should be.


John

Mike Reed

unread,
Sep 23, 2014, 6:05:26 PM9/23/14
to puppet...@googlegroups.com
Hey John,

Thanks for the reply and comments to my questions.  I understand what you're saying and the ideas of include/inherit/contain make better sense now.

Cheers,

Mike.


On Friday, September 19, 2014 4:01:28 PM UTC-7, Mike Reed wrote:
Reply all
Reply to author
Forward
0 new messages