├── .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 |
--------------------------------------------------------------------------------