File Coverage

blib/lib/Config/Model/Backend/Systemd/Unit.pm
Criterion Covered Total %
statement 143 147 97.2
branch 41 52 78.8
condition 14 24 58.3
subroutine 18 18 100.0
pod 2 5 40.0
total 218 246 88.6


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             $Config::Model::Backend::Systemd::Unit::VERSION = '0.252.1';
11             use strict;
12 3     3   13966 use warnings;
  3         14  
  3         91  
13 3     3   17 use 5.020;
  3         6  
  3         116  
14 3     3   76 use Mouse ;
  3         12  
15 3     3   19 use Log::Log4perl qw(get_logger :levels);
  3         7  
  3         23  
16 3     3   1591 use Path::Tiny;
  3         9  
  3         24  
17 3     3   406  
  3         6  
  3         235  
18             use feature qw/postderef signatures/;
19 3     3   22 no warnings qw/experimental::postderef experimental::signatures/;
  3         7  
  3         392  
20 3     3   35  
  3         32  
  3         6591  
21             extends 'Config::Model::Backend::IniFile';
22              
23             with 'Config::Model::Backend::Systemd::Layers';
24              
25             my $logger = get_logger("Backend::Systemd::Unit");
26             my $user_logger = get_logger("User");
27              
28             # get info from tree when Unit is children of systemd (app is systemd)
29 46     46 0 135 my $unit_type = $self->node->element_name;
  46         134  
  46         117  
  46         181  
30             my $unit_name = $self->node->index_value;
31 46         261 my $app = $self->instance->application;
32 46         237 my ($trash, $app_type) = split /-/, $app;
33 46         254  
34 46         594 # get info from file name (app is systemd-* not -user)
35             if (my $fp = $file_path->basename) {
36             my ($n,$t) = split /\./, $fp;
37 46 50       265 $unit_type ||= $t;
38 46         1476 $unit_name ||= $n;
39 46   66     261 }
40 46   66     219  
41             # fallback to app type when file is name without unit type
42             $unit_type ||= $app_type if ($app_type and $app_type ne 'user');
43              
44 46 100 33     350 Config::Model::Exception::User->throw(
      100        
45             object => $self,
46 46 50       197 error => "Unknown unit type. Please add type to file name. e.g. "
47             . $file_path->basename.".service or socket..."
48             ) unless $unit_type;
49              
50             # safety check
51             if ($app !~ /^systemd(-user)?$/ and $app !~ /^systemd-$unit_type/) {
52             Config::Model::Exception::User->throw(
53 46 50 66     610 objet => $self->node,
54 0         0 error => "Unit type $unit_type does not match app $app"
55             );
56             }
57              
58             return ($unit_name, $unit_type);
59             }
60 46         256  
61             ## no critic (Subroutines::ProhibitBuiltinHomonyms)
62             # enable 2 styles of comments (gh #1)
63             $args{comment_delimiter} = "#;";
64 34     34 1 1465598  
  34         107  
  34         285  
  34         114  
65             # args are:
66 34         115 # root => './my_test', # fake root directory, used for tests
67             # config_dir => /etc/foo', # absolute path
68             # file => 'foo.conf', # file name
69             # file_path => './my_test/etc/foo/foo.conf'
70             # check => yes|no|skip
71              
72             if ($self->instance->application =~ /-file$/) {
73             # allow non-existent file to let user start from scratch
74             return 1 unless $args{file_path}->exists;
75 34 100       201  
76             return $self->load_ini_file(%args);
77 7 100       153 }
78              
79 5         179 my ($unit_name, $unit_type) = $self->get_unit_info($args{file_path});
80             my $app = $self->instance->application;
81              
82 27         510 $self->node->instance->layered_start;
83 27         164 my $root = $args{root} || path('/');
84             my $cwd = $args{root} || path('.');
85 27         359  
86 27   33     712 # load layers for this service
87 27   33     142 my $found_unit = 0;
88             foreach my $layer ($self->default_directories) {
89             my $local_root = $layer =~ m!^/! ? $root : $cwd;
90 27         67 my $layer_dir = $local_root->child($layer);
91 27         179 next unless $layer_dir->is_dir;
92 125 50       2039  
93 125         434 my $layer_file = $layer_dir->child($unit_name.'.'.$unit_type);
94 125 100       4513 next unless $layer_file->exists;
95              
96 30         804 $user_logger->warn("Reading unit '$unit_type' '$unit_name' from '$layer_file'.");
97 30 100       1192 $self->load_ini_file(%args, file_path => $layer_file);
98             $found_unit++;
99 16         426  
100 16         419 # TODO: may also need to read files in
101 16         133 # $unit_name.'.'.$unit_type.'.d' to get all default values
102             # (e.g. /lib/systemd/system/rc-local.service.d/debian.conf)
103             }
104             $self->node->instance->layered_stop;
105              
106             if (not $found_unit) {
107 27         684 $user_logger->warn("Could not find unit files for $unit_type name $unit_name");
108             }
109 27 100       586  
110 11         66 # now read editable file (files that can be edited with systemctl edit <unit>.<type>
111             # for systemd -> /etc/ systemd/system/unit.type.d/override.conf
112             # for user -> ~/.local/systemd/user/*.conf
113             # for local file -> $args{filexx}
114              
115             # TODO: document limitations (can't read arbitrary files in /etc/
116             # systemd/system/unit.type.d/ and
117             # ~/.local/systemd/user/unit.type.d/*.conf
118              
119             my $service_path;
120             if ($app =~ /-user$/) {
121             $service_path = $args{file_path} ;
122 27         166 }
123 27 100       165 else {
124 10         44 $service_path = $args{file_path}->parent->child("$unit_name.$unit_type.d/override.conf");
125             }
126              
127 17         107 if ($service_path->exists and $service_path->realpath eq '/dev/null') {
128             $logger->debug("skipping unit $unit_type name $unit_name from $service_path");
129             }
130 27 50 66     1829 elsif ($service_path->exists) {
    100          
131 0         0 $logger->debug("reading unit $unit_type name $unit_name from $service_path");
132             $self->load_ini_file(%args, file_path => $service_path);
133             }
134 21         7387 return 1;
135 21         546 }
136              
137 27         754 my ($self, %args) = @_ ;
138              
139             $logger->debug("opening file '".$args{file_path}."' to read");
140              
141 42     42 0 424 my $res = $self->SUPER::read( %args );
142             die "failed ". $args{file_path}." read" unless $res;
143 42         196 return;
144             }
145 42         842  
146 42 50       9374 # overrides call to node->load_data
147 42         358 my $check = $args{check};
148             my $data = $args{data} ;
149              
150             my $disp_leaf = sub {
151 42     42 0 48354 my ($scanner, $data, $node,$element_name,$index, $leaf_object) = @_ ;
  42         113  
  42         166  
  42         100  
152 42         131 if (ref($data) eq 'ARRAY') {
153 42         107 Config::Model::Exception::User->throw(
154             object => $leaf_object,
155             error => "Cannot store twice the same value ('"
156 114     114   581113 .join("', '",@$data). "'). "
157 114 50       469 ."Is '$element_name' line duplicated in config file ? "
158 0 0       0 ."You can use -force option to load value '". $data->[-1]."'."
159             ) if $check eq 'yes';
160             $data = $data->[-1];
161             }
162             # remove this translation after Config::Model 2.146
163             if ($leaf_object->value_type eq 'boolean') {
164             $data = 'yes' if $data eq 'on';
165 0         0 $data = 'no' if $data eq 'off';
166             }
167             $leaf_object->store(value => $data, check => $check);
168 114 100       465 } ;
169 9 100       49  
170 9 50       46 my $unit_cb = sub {
171             my ($scanner, $data_ref,$node,@elements) = @_ ;
172 114         553  
173 42         329 # read data in the model order
174             foreach my $elt (@elements) {
175             my $unit_data = delete $data_ref->{$elt}; # extract relevant data
176 131     131   4527098 next unless defined $unit_data;
177             $scanner->scan_element($unit_data, $node,$elt) ;
178             }
179 131         446 # read accepted elements
180 9688         206263 foreach my $elt (sort keys %$data_ref) {
181 9688 100       18699 my $unit_data = $data_ref->{$elt}; # extract relevant data
182 326         1412 $scanner->scan_element($unit_data, $node,$elt) ;
183             }
184             };
185 131         1565  
186 2         9 # this setup is required because IniFile backend cannot push value
187 2         9 # coming from several ini files on a single list element. (even
188             # though keys can be repeated in a single ini file and stored as
189 42         242 # list in a single config element, this is not possible if the
190             # list values come from several files)
191             my $list_cb = sub {
192             my ($scanner, $data,$node,$element_name,@idx) = @_ ;
193             my $list_ref = ref($data) ? $data : [ $data ];
194             my $list_obj= $node->fetch_element(name => $element_name, check => $check);
195             foreach my $d (@$list_ref) {
196             $list_obj->push($d); # push also empty values
197 125     125   22215 }
198 125 100       496  
199 125         438 };
200 125         8906  
201 131         6698 my $scan = Config::Model::ObjTreeScanner-> new (
202             node_content_cb => $unit_cb,
203             list_element_cb => $list_cb,
204 42         257 leaf_cb => $disp_leaf,
205             ) ;
206 42         513  
207             $scan->scan_node($data, $self->node) ;
208             return;
209             }
210              
211             # args are:
212 42         9132 # root => './my_test', # fake root directory, userd for tests
213 42         2221 # config_dir => /etc/foo', # absolute path
214             # file => 'foo.conf', # file name
215             # file_path => './my_test/etc/foo/foo.conf'
216 20     20 1 3116307 # check => yes|no|skip
  20         69  
  20         134  
  20         49  
217              
218             if ($self->node->grab_value('disable')) {
219             my $fp = $args{file_path};
220             if ($fp->realpath ne '/dev/null') {
221             $user_logger->warn("symlinking file $fp to /dev/null");
222             $fp->remove;
223             symlink ('/dev/null', $fp->stringify);
224 20 100       230 }
225 1         692 return 1;
226 1 50       23 }
227 1         238  
228 1         22 my ($unit_name, $unit_type) = $self->get_unit_info($args{file_path});
229 1         66  
230             my $app = $self->instance->application;
231 1         63 my $service_path;
232             if ($app =~ /-(user|file)$/) {
233             $service_path = $args{file_path};
234 19         26546  
235             $logger->debug("writing unit to $service_path");
236 19         85 # mouse super() does not work...
237 19         149 $self->SUPER::write(%args, file_path => $service_path);
238 19 100       134 }
239 10         34 else {
240             my $dir = $args{file_path}->parent->child("$unit_name.$unit_type.d");
241 10         53 $dir->mkpath({ mode => oct(755) });
242             $service_path = $dir->child('override.conf');
243 10         258  
244             $logger->debug("writing unit to $service_path");
245             # mouse super() does not work...
246 9         49 $self->SUPER::write(%args, file_path => $service_path);
247 9         864  
248 9         997 if (scalar $dir->children == 0) {
249             # remove empty dir
250 9         414 $logger->warn("Removing empty dir $dir");
251             rmdir $dir;
252 9         227 }
253             }
254 9 100       13069 return 1;
255             }
256 2         307  
257 2         47 my ($self, $args, $node, $elt) = @_ ;
258             # must skip disable element which cannot be hidden :-(
259             if ($elt eq 'disable') {
260 19         18268 return '';
261             } else {
262             return $self->SUPER::_write_leaf($args, $node, $elt);
263             }
264 4614     4614   2735784 }
265              
266 4614 100       10460 no Mouse ;
267 19         95 __PACKAGE__->meta->make_immutable ;
268              
269 4595         11767 1;
270              
271             # ABSTRACT: R/W backend for systemd unit files
272              
273 3     3   26  
  3         15  
  3         23  
274             =pod
275              
276             =encoding UTF-8
277              
278             =head1 NAME
279              
280             Config::Model::Backend::Systemd::Unit - R/W backend for systemd unit files
281              
282             =head1 VERSION
283              
284             version 0.252.1
285              
286             =head1 SYNOPSIS
287              
288             # in systemd service or socket model
289             rw_config => {
290             'auto_create' => '1',
291             'auto_delete' => '1',
292             'backend' => 'Systemd::Unit',
293             'file' => '&index.service'
294             }
295              
296             =head1 DESCRIPTION
297              
298             C<Config::Model::Backend::Systemd::Unit> provides a plugin class to enable
299             L<Config::Model> to read and write systemd configuration files. This
300             class inherits L<Config::Model::Backend::IniFile> is designed to be used
301             by L<Config::Model::BackendMgr>.
302              
303             =head1 Methods
304              
305             =head2 read
306              
307             This method read config data from systemd default file to get default
308             values and read config data.
309              
310             =head2 write
311              
312             This method write systemd configuration data.
313              
314             When the service is disabled, the target configuration file is
315             replaced by a link to C</dev/null>.
316              
317             =head1 AUTHOR
318              
319             Dominique Dumont
320              
321             =head1 COPYRIGHT AND LICENSE
322              
323             This software is Copyright (c) 2008-2022 by Dominique Dumont.
324              
325             This is free software, licensed under:
326              
327             The GNU Lesser General Public License, Version 2.1, February 1999
328              
329             =cut