File Coverage

blib/lib/Config/Model/Backend/Systemd.pm
Criterion Covered Total %
statement 108 132 81.8
branch 28 44 63.6
condition 13 24 54.1
subroutine 13 14 92.8
pod 2 5 40.0
total 164 219 74.8


line stmt bran cond sub pod time code
1             #
2             # This file is part of Config-Model-Systemd
3             #
4             # This software is Copyright (c) 2008-2022 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package Config::Model::Backend::Systemd ;
11             $Config::Model::Backend::Systemd::VERSION = '0.252.2';
12 2     2   7463 use strict;
  2         4  
  2         52  
13 2     2   9 use warnings;
  2         4  
  2         43  
14 2     2   45 use 5.020;
  2         7  
15 2     2   9 use Mouse ;
  2         3  
  2         14  
16 2     2   939 use Log::Log4perl qw(get_logger :levels);
  2         4  
  2         15  
17 2     2   274 use Path::Tiny 0.086;
  2         50  
  2         151  
18              
19             extends 'Config::Model::Backend::Any';
20             with 'Config::Model::Backend::Systemd::Layers';
21              
22 2     2   12 use feature qw/postderef signatures/;
  2         4  
  2         192  
23 2     2   10 no warnings qw/experimental::postderef experimental::signatures/;
  2         5  
  2         3020  
24              
25             my $logger = get_logger("Backend::Systemd");
26             my $user_logger = get_logger("User");
27              
28             has config_dir => (
29             is => 'rw',
30             isa => 'Path::Tiny'
31             );
32              
33             has 'annotation' => ( is => 'ro', isa => 'Bool', default => 1 );
34              
35             # TODO: accepts other systemd suffixes
36             my @service_types = qw/service socket/;
37             my $joined_types = join('|', @service_types);
38             my $filter = qr/\.($joined_types)(\.d)?$/;
39              
40             sub get_backend_arg {
41 29     29 0 79 my $self = shift ;
42              
43 29         99 my $ba = $self->instance->backend_arg;
44 29 50       381 if (not $ba) {
45 0         0 Config::Model::Exception::User->throw(
46             objet => $self->node,
47             error => "Missing systemd unit to work on. This may be passed as 3rd argument to cme",
48             );
49             }
50 29         95 return $ba;
51             }
52              
53             ## no critic (Subroutines::ProhibitBuiltinHomonyms)
54 23     23 1 2439243 sub read ($self, @args) {
  23         61  
  23         120  
  23         50  
55 23         113 my $app = $self->instance->application;
56              
57 23 50       458 if ($app =~ /file/) {
58 0         0 return $self->read_systemd_files(@args);
59             }
60             else {
61 23         123 return $self->read_systemd_units(@args);
62             }
63             }
64              
65 0     0 0 0 sub read_systemd_files ($self, %args) {
  0         0  
  0         0  
  0         0  
66             # args are:
67             # root => './my_test', # fake root directory, used for tests
68             # config_dir => /etc/foo', # absolute path
69             # config_file => 'foo.conf', # file name
70             # file_path => './my_test/etc/foo/foo.conf'
71             # check => yes|no|skip
72              
73             #use Tk::ObjScanner; Tk::ObjScanner::scan_object(\%args) ;
74 0         0 my $file = $args{file_path};
75 0 0       0 if (not $file) {
76 0         0 Config::Model::Exception::User->throw(
77             objet => $self->node,
78             error => "Missing systemd file to work on. This may be passed as 3rd argument to cme",
79             );
80             }
81              
82 0         0 $user_logger->warn( "Loading unit file '$file'");
83 0         0 my ($service_name, $unit_type) = split /\./, path($file)->basename;
84              
85 0 0       0 my @to_create = $unit_type ? ($unit_type) : @service_types;
86 0         0 foreach my $unit_type (@to_create) {
87 0         0 $logger->debug("registering unit $unit_type name $service_name from file name");
88 0         0 $self->node->load(step => qq!$unit_type:"$service_name"!, check => $args{check} ) ;
89             }
90 0         0 return 1;
91             }
92              
93 23     23 0 50 sub read_systemd_units ($self, %args) {
  23         42  
  23         138  
  23         36  
94             # args are:
95             # root => './my_test', # fake root directory, used for tests
96             # config_dir => /etc/foo', # absolute path
97             # config_file => 'foo.conf', # file name
98             # file_path => './my_test/etc/foo/foo.conf'
99             # check => yes|no|skip
100              
101 23         80 my $app = $self->instance->application;
102              
103 23         302 my $select_unit = $self->get_backend_arg;
104 23 50       88 if (not $select_unit) {
105 0         0 Config::Model::Exception::User->throw(
106             objet => $self->node,
107             error => "Missing systemd unit to work on. This must be passed as 3rd argument to cme",
108             );
109             }
110              
111 23 50 66     207 if ($app ne 'systemd-user' and $select_unit =~ $filter) {
112 0         0 my $unit_type = $1;
113 0 0       0 if ($app eq 'systemd') {
    0          
114 0         0 Config::Model::Exception::User->throw(
115             objet => $self->node,
116             error => "With 'systemd' app, unit name should not specify '$unit_type'. "
117             ." Use 'systemd-$unit_type' app if you want to act only on $select_unit",
118             );
119             }
120             elsif ($app ne 'systemd-$unit_type') {
121 0         0 Config::Model::Exception::User->throw(
122             objet => $self->node,
123             error => "Unit name $select_unit does not match app $app"
124             );
125             }
126             }
127              
128 23 50       96 if ($select_unit ne '*') {
129 23         132 $user_logger->warn( "Loading unit matching '$select_unit'");
130             } else {
131 0         0 $user_logger->warn("Loading all units...")
132             }
133              
134 23   33     332 my $root_path = $args{root} || path('/');
135              
136             # load layers. layered mode is handled by Unit backend. Only a hash
137             # key is created here, so layered mode does not matter
138 23         130 foreach my $layer ($self->default_directories) {
139 103         12939 my $dir = $root_path->child($layer);
140 103 100       3498 next unless $dir->is_dir;
141 22         479 $self->config_dir($dir);
142              
143 22         97 foreach my $file ($dir->children($filter) ) {
144 30         9403 my $file_name = $file->basename();
145 30         724 my $unit_name = $file->basename($filter);
146 30         791 $logger->trace( "checking unit $file_name from $file (layered mode) against $select_unit");
147 30 100 66     638 if ($select_unit ne '*' and $file_name !~ /$select_unit/) {
148 8         26 $logger->trace( "unit $file_name from $file (layered mode) does not match $select_unit");
149 8         84 next;
150             }
151 22         150 my ($unit_type) = ($file =~ $filter);
152 22         244 $logger->debug( "registering unit $unit_type name $unit_name from $file (layered mode))");
153             # force config_dir during init
154 22         363 $self->node->load(step => qq!$unit_type:"$unit_name"!, check => $args{check} ) ;
155             }
156             }
157              
158 23         22655 my $dir = $root_path->child($args{config_dir});
159              
160 23 100       980 if (not $dir->is_dir) {
161 4         70 $logger->debug("skipping missing directory $dir");
162 4         86 return 1 ;
163             }
164              
165 19         539 $self->config_dir($dir);
166 19         57 my $found = 0;
167 19         75 foreach my $file ($dir->children($filter) ) {
168 24         2269 my ($unit_type,$dot_d) = ($file =~ $filter);
169 24         265 my $file_name = $file->basename();
170 24         565 my $unit_name = $file->basename($filter);
171              
172 24 100 66     712 next if ($select_unit ne '*' and $file_name !~ /$select_unit/);
173 20         85 $logger->trace( "checking $file against $select_unit");
174              
175 20 100 66     295 if ($file->realpath eq '/dev/null') {
    100          
176 1         216 $logger->warn("unit $unit_type name $unit_name from $file is disabled");
177 1         19 $self->node->load(step => qq!$unit_type:"$unit_name" disable=1!, check => $args{check} ) ;
178             }
179             elsif ($dot_d and $file->child('override.conf')->exists) {
180 12         3019 $logger->warn("registering unit $unit_type name $unit_name from override file");
181 12         199 $self->node->load(step => qq!$unit_type:"$unit_name"!, check => $args{check} ) ;
182             }
183             else {
184 7         1711 $logger->warn("registering unit $unit_type name $unit_name from $file");
185 7         178 $self->node->load(step => qq!$unit_type:"$unit_name"!, check => $args{check} ) ;
186             }
187 20         38797 $found++;
188             }
189              
190 19 100       166 if (not $found) {
191             # no service exists, let's create them.
192 2         12 $user_logger->warn( "No unit '$select_unit' found in $dir, creating one...");
193 2         46 my ($service_name, $unit_type) = split /\./, $select_unit;
194 2 100       10 my @to_create = $unit_type ? ($unit_type) : @service_types;
195 2   33     9 $service_name //= $select_unit;
196 2         6 foreach my $unit_type (@to_create) {
197 3         663 $logger->debug("registering unit $unit_type name $service_name from scratch");
198 3         36 $self->node->load(step => qq!$unit_type:"$service_name"!, check => $args{check} ) ;
199             }
200             }
201 19         3568 return 1 ;
202             }
203              
204 13     13 1 4386566 sub write ($self, %args) {
  13         35  
  13         60  
  13         27  
205             # args are:
206             # root => './my_test', # fake root directory, userd for tests
207             # config_dir => /etc/foo', # absolute path
208             # file => 'foo.conf', # file name
209             # file_path => './my_test/etc/foo/foo.conf'
210             # check => yes|no|skip
211              
212             # file write is handled by Unit backend
213 13 50       94 return 1 if $self->instance->application =~ /file/;
214 13 100       379 return 1 if $self->instance->application eq 'systemd';
215              
216 6   33     102 my $root_path = $args{root} || path('/');
217 6         39 my $dir = $args{root}->path($args{config_dir});
218 6 50       290 die "Unknown directory $dir" unless $dir->is_dir;
219              
220 6         142 my $select_unit = $self->get_backend_arg;
221              
222             # delete files for non-existing elements (deleted services)
223 6         31 foreach my $file ($dir->children($filter) ) {
224 4         683 my ($unit_type) = ($file =~ $filter);
225 4         47 my $unit_name = $file->basename($filter);
226              
227 4 100 66     282 next if ($select_unit ne '*' and $unit_name !~ /$select_unit/);
228              
229 3         22 my $unit_collection = $self->node->fetch_element($unit_type);
230 3 50       242 if (not $unit_collection->defined($unit_name)) {
231 0         0 $user_logger->warn("removing file $file of deleted service");
232 0         0 $file->remove;
233             }
234             }
235              
236 6         282 return 1;
237             }
238              
239 2     2   30 no Mouse ;
  2         5  
  2         11  
240             __PACKAGE__->meta->make_immutable ;
241              
242             1;
243              
244             # ABSTRACT: R/W backend for systemd configurations files
245              
246             __END__
247              
248             =pod
249              
250             =encoding UTF-8
251              
252             =head1 NAME
253              
254             Config::Model::Backend::Systemd - R/W backend for systemd configurations files
255              
256             =head1 VERSION
257              
258             version 0.252.2
259              
260             =head1 SYNOPSIS
261              
262             # in systemd model
263             rw_config => {
264             'backend' => 'Systemd'
265             }
266              
267             =head1 DESCRIPTION
268              
269             Config::Model::Backend::Systemd provides a plugin class to enable
270             L<Config::Model> to read and write systemd configuration files. This
271             class inherits L<Config::Model::Backend::Any> is designed to be used
272             by L<Config::Model::BackendMgr>.
273              
274             =head1 Methods
275              
276             =head2 read
277              
278             This method scans systemd default directory and systemd config
279             directory to create all units in L<Config::Model> tree. The actual
280             configuration parameters are read by
281             L<Config::Model::Backend::Systemd::Unit>.
282              
283             =head2 write
284              
285             This method is a bit of a misnomer. It deletes configuration files of
286             deleted service.
287              
288             The actual configuration parameters are written by
289             L<Config::Model::Backend::Systemd::Unit>.
290              
291             =head1 AUTHOR
292              
293             Dominique Dumont
294              
295             =head1 COPYRIGHT AND LICENSE
296              
297             This software is Copyright (c) 2008-2022 by Dominique Dumont.
298              
299             This is free software, licensed under:
300              
301             The GNU Lesser General Public License, Version 2.1, February 1999
302              
303             =cut