├── .gitignore ├── examples └── init.pp ├── lib ├── puppet │ ├── type │ │ └── profile_manager.rb │ └── provider │ │ └── profile_manager │ │ └── macos.rb └── facter │ └── profiles.rb ├── metadata.json ├── manifests └── manage.pp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | pkg 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /examples/init.pp: -------------------------------------------------------------------------------- 1 | mac_profiles_handler::manage { 'com.puppetlabs.myprofile': 2 | ensure => present, 3 | file_source => 'puppet:///modules/mymodule/com.puppetlabs.myprofile.mobileconfig', 4 | } 5 | -------------------------------------------------------------------------------- /lib/puppet/type/profile_manager.rb: -------------------------------------------------------------------------------- 1 | Puppet::Type.newtype(:profile_manager) do 2 | @doc = <<-EOT 3 | Manage Apple Configuration Profiles 4 | http://help.apple.com/profilemanager/mac/10.7/#apd88330954-6FA0-4568-A88E-7F6828E763A7 5 | 6 | Example Usage: 7 | profile_manager { 'com.puppetlabs.foo': 8 | ensure => present, 9 | profile => '/path/to/profile.mobileconfig', 10 | } 11 | 12 | The namevar for this type is the identifier for the profile. 13 | Profile = path to the profile on the client system. 14 | 15 | 16 | EOT 17 | 18 | ensurable 19 | 20 | def refresh 21 | provider.create 22 | end 23 | 24 | newparam(:name, namevar: true) 25 | newparam(:profile) 26 | end 27 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-mac_profiles_handler", 3 | "author": "", 4 | "license": "", 5 | "version": "2.0.0", 6 | "summary": "Puppet Module for managing macOS Configuration Profiles", 7 | "source": "https://github.com/keeleysam/puppet-mac_profiles_handler", 8 | "project_page": "https://github.com/keeleysam/puppet-mac_profiles_handler", 9 | "issues_url": "https://github.com/keeleysam/puppet-mac_profiles_handler/issues", 10 | "tags": [ 11 | "macOS", 12 | "OS X", 13 | "mobileconfig", 14 | "profiles" 15 | ], 16 | "operatingsystem_support": [ 17 | { 18 | "operatingsystem": "Darwin" 19 | } 20 | ], 21 | "requirements": [ 22 | { 23 | "name": "puppet", 24 | "version_requirement": ">= 4.4.0" 25 | } 26 | ], 27 | "dependencies": [ 28 | { 29 | "name": "puppetlabs/stdlib", 30 | "version_requirement": ">= 2.3.1" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /lib/facter/profiles.rb: -------------------------------------------------------------------------------- 1 | Facter.add(:profiles) do 2 | confine kernel: 'Darwin' 3 | setcode do 4 | 5 | require 'puppet/util/plist' 6 | require 'time' 7 | 8 | profiles = {} 9 | 10 | if Facter.value(:os)['release']['major'].to_i >= 12 11 | 12 | plist = Puppet::Util::Plist.parse_plist(Facter::Util::Resolution.exec(['/usr/bin/profiles', '-C', '-o', 'stdout-xml'].join(' '))) 13 | 14 | if plist.key?('_computerlevel') 15 | for item in plist['_computerlevel'] 16 | profiles[item['ProfileIdentifier']] = { 17 | 'display_name' => item['ProfileDisplayName'], 18 | 'description' => item['ProfileDescription'], 19 | 'verification_state' => item['ProfileVerificationState'], 20 | 'uuid' => item['ProfileUUID'], 21 | 'organization' => item['ProfileOrganization'], 22 | 'type' => item['ProfileType'], 23 | 'install_date' => DateTime.parse(item['ProfileInstallDate']), 24 | 'payload' => [] 25 | } 26 | 27 | for pl in item['ProfileItems'] 28 | profiles[item['ProfileIdentifier']]['payload'] << { 29 | 'type' => pl['PayloadType'], 30 | 'identifier' => pl['PayloadIdentifier'], 31 | 'uuid' => pl['PayloadUUID'], 32 | # commented out for now because its not super useful. 33 | # 'content' => pl['PayloadContent'], 34 | } 35 | end 36 | end 37 | end 38 | end 39 | 40 | profiles 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /manifests/manage.pp: -------------------------------------------------------------------------------- 1 | # manage mac profiles 2 | define mac_profiles_handler::manage( 3 | $file_source = '', 4 | $ensure = 'present', 5 | $type = 'mobileconfig', 6 | ) { 7 | 8 | if $facts['os']['name'] != 'Darwin' { 9 | fail('The mobileconfig::manage resource type is only supported on macOS') 10 | } 11 | 12 | case $ensure { 13 | 'absent': { 14 | profile_manager { $name: 15 | ensure => $ensure, 16 | } 17 | } 18 | default: { 19 | File { 20 | owner => 'root', 21 | group => 'wheel', 22 | mode => '0700', 23 | } 24 | 25 | if ! defined(File["${facts['puppet_vardir']}/mobileconfigs"]) { 26 | file { "${facts['puppet_vardir']}/mobileconfigs": 27 | ensure => directory, 28 | } 29 | } 30 | case $type { 31 | 'template': { 32 | file { "${facts['puppet_vardir']}/mobileconfigs/${name}": 33 | ensure => file, 34 | content => $file_source, 35 | } 36 | } 37 | default: { 38 | file { "${facts['puppet_vardir']}/mobileconfigs/${name}": 39 | ensure => file, 40 | source => $file_source, 41 | } 42 | } 43 | } 44 | profile_manager { $name: 45 | ensure => $ensure, 46 | profile => "${facts['puppet_vardir']}/mobileconfigs/${name}", 47 | require => File["${facts['puppet_vardir']}/mobileconfigs/${name}"], 48 | subscribe => File["${facts['puppet_vardir']}/mobileconfigs/${name}"], 49 | } 50 | } 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mac_profiles_handler module for Puppet 2 | 3 | ## Description 4 | This module provides two resource types for interacting with macOS configuration profiles. 5 | 6 | The profile_manager resource type is the back-end type that interacts with /usr/bin/profiles for creating, destroying and verifying a resource type. The mac_profiles_handler::manage resource type is user-facing and handles the management of the actual files. 7 | 8 | A structured fact is also provided to list installed profiles along with some metadata. 9 | 10 | ## Usage 11 | 12 |
13 | mac_profiles_handler::manage { 'com.puppetlabs.myprofile':  
14 |   ensure  => present,  
15 |   file_source => 'puppet:///modules/mymodule/com.puppetlabs.myprofile.mobileconfig',  
16 | }
17 | 
18 | 19 | You can use an ERB template instead of a mobileconfig: 20 |
21 | mac_profiles_handler::manage { 'com.puppetlabs.myprofile':  
22 |   ensure  => present,  
23 |   file_source => template('mymodule/com.puppetlabs.myprofile.erb'),  
24 |   type => 'template',  
25 | }
26 | 
27 | 28 | You can also ensure that a profile is absent by specifying just the identifier: 29 |
30 | mac_profiles_handler::manage { '00000000-0000-0000-A000-4A414D460003':
31 |   ensure => absent,
32 | }
33 | 
34 | 35 | 36 | You must pass the profilers identifier as your namevar, ensure accepts present or absent and file_source behaves the same way source behaves for file. 37 | 38 | ## Dependencies 39 | 40 | * [puppetlabs/stdlib >= 2.3.1](https://forge.puppetlabs.com/puppetlabs/stdlib) 41 | * Puppet >= 4.4.0 for `puppet/util/plist`, for earlier versions use d13469a. 42 | 43 | ## To-Do 44 | Improve provider parsing. 45 | Handle more types of configuration profiles. 46 | Improve documentation when author isn't presenting the next morning. 47 | 48 | ## Contributing 49 | Please do! 50 | Create issues in GitHub, Make Pull Requests, Have Fun! 51 | -------------------------------------------------------------------------------- /lib/puppet/provider/profile_manager/macos.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/util/plist' 2 | 3 | Puppet::Type.type(:profile_manager).provide :macos do 4 | desc 'Provides management of mobileconfig profiles on macOS.' 5 | 6 | confine operatingsystem: :darwin 7 | 8 | defaultfor operatingsystem: :darwin 9 | 10 | commands profilescmd: '/usr/bin/profiles' 11 | 12 | def create 13 | profilescmd('-I', '-F', resource[:profile]) 14 | writereceipt 15 | end 16 | 17 | def destroy 18 | profilescmd('-R', '-p', resource[:name]) 19 | end 20 | 21 | def exists? 22 | # if already installed, check if it is the right one. 23 | # if not installed, return false. 24 | # if we are removing, don't care if it is the right one. 25 | state = getinstalledstate 26 | if state != false 27 | if resource[:ensure] == :absent 28 | return true 29 | else 30 | begin 31 | return state['install_date'].to_time == getreceipts[resource[:name]]['install_date'] 32 | rescue NoMethodError 33 | # no matching receipt 34 | return false 35 | end 36 | end 37 | else 38 | return false 39 | end 40 | end 41 | 42 | def getreceipts 43 | begin 44 | receipts = Puppet::Util::Plist.read_plist_file(Puppet[:vardir] + '/mobileconfigs/receipts.plist') 45 | rescue IOError, Errno::ENOENT 46 | receipts = {} 47 | end 48 | receipts 49 | end 50 | 51 | def writereceipt 52 | # get install time from profile, write to disk so we know if the 53 | # currently installed profile is the one we installed, this uses 54 | # code from the fact but needs to re-run immediately. 55 | receipts = getreceipts 56 | 57 | receipts[resource[:name]] = { 'install_date' => getinstalledstate['install_date'] } 58 | 59 | Puppet::Util::Plist.write_plist_file(receipts, Puppet[:vardir] + '/mobileconfigs/receipts.plist') 60 | end 61 | 62 | def getinstalledstate 63 | 64 | plist = Puppet::Util::Plist.parse_plist(Puppet::Util::Execution.execute(['/usr/bin/profiles', '-C', '-o', 'stdout-xml'])) 65 | 66 | if plist.key?('_computerlevel') 67 | for item in plist['_computerlevel'] 68 | if item['ProfileIdentifier'] == resource[:name] 69 | return { 70 | 'identifier' => item['ProfileIdentifier'], 71 | 'display_name' => item['ProfileDisplayName'], 72 | 'uuid' => item['ProfileUUID'], 73 | 'install_date' => DateTime.parse(item['ProfileInstallDate']) 74 | } 75 | end 76 | end 77 | end 78 | return false 79 | end 80 | end 81 | --------------------------------------------------------------------------------