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.251.1';
11             use strict;
12 3     3   15716 use warnings;
  3         7  
  3         76  
13 3     3   13 use 5.020;
  3         5  
  3         72  
14 3     3   60 use Mouse ;
  3         10  
15 3     3   15 use Log::Log4perl qw(get_logger :levels);
  3         6  
  3         20  
16 3     3   1491 use Path::Tiny;
  3         7  
  3         26  
17 3     3   481  
  3         8  
  3         186  
18             use feature qw/postderef signatures/;
19 3     3   18 no warnings qw/experimental::postderef experimental::signatures/;
  3         5  
  3         376  
20 3     3   16  
  3         27  
  3         5236  
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 93 my $unit_type = $self->node->element_name;
  46         85  
  46         97  
  46         103  
30             my $unit_name = $self->node->index_value;
31 46         200 my $app = $self->instance->application;
32 46         165 my ($trash, $app_type) = split /-/, $app;
33 46         158  
34 46         664 # 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       191 $unit_type ||= $t;
38 46         1138 $unit_name ||= $n;
39 46   66     171 }
40 46   66     138  
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     286 Config::Model::Exception::User->throw(
      100        
45             object => $self,
46 46 50       139 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     383 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         188  
61             ## no critic (Subroutines::ProhibitBuiltinHomonyms)
62             # enable 2 styles of comments (gh #1)
63             $args{comment_delimiter} = "#;";
64 34     34 1 1114421  
  34         81  
  34         219  
  34         63  
65             # args are:
66 34         91 # 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       382  
76             return $self->load_ini_file(%args);
77 7 100       153 }
78              
79 5         93 my ($unit_name, $unit_type) = $self->get_unit_info($args{file_path});
80             my $app = $self->instance->application;
81              
82 27         541 $self->node->instance->layered_start;
83 27         112 my $root = $args{root} || path('/');
84             my $cwd = $args{root} || path('.');
85 27         384  
86 27   33     453 # load layers for this service
87 27   33     112 my $found_unit = 0;
88             foreach my $layer ($self->default_directories) {
89             my $local_root = $layer =~ m!^/! ? $root : $cwd;
90 27         61 my $layer_dir = $local_root->child($layer);
91 27         146 next unless $layer_dir->is_dir;
92 125 50       1469  
93 125         346 my $layer_file = $layer_dir->child($unit_name.'.'.$unit_type);
94 125 100       3306 next unless $layer_file->exists;
95              
96 30         516 $user_logger->warn("Reading unit '$unit_type' '$unit_name' from '$layer_file'.");
97 30 100       776 $self->load_ini_file(%args, file_path => $layer_file);
98             $found_unit++;
99 16         273  
100 16         300 # TODO: may also need to read files in
101 16         108 # $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         441 $user_logger->warn("Could not find unit files for $unit_type name $unit_name");
108             }
109 27 100       459  
110 11         52 # 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         153 }
123 27 100       135 else {
124 10         28 $service_path = $args{file_path}->parent->child("$unit_name.$unit_type.d/override.conf");
125             }
126              
127 17         76 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     1259 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         4729 return 1;
135 21         364 }
136              
137 27         550 my ($self, %args) = @_ ;
138              
139             $logger->debug("opening file '".$args{file_path}."' to read");
140              
141 42     42 0 282 my $res = $self->SUPER::read( %args );
142             die "failed ". $args{file_path}." read" unless $res;
143 42         135 return;
144             }
145 42         567  
146 42 50       7292 # overrides call to node->load_data
147 42         270 my $check = $args{check};
148             my $data = $args{data} ;
149              
150             my $disp_leaf = sub {
151 42     42 0 34348 my ($scanner, $data, $node,$element_name,$index, $leaf_object) = @_ ;
  42         65  
  42         135  
  42         61  
152 42         88 if (ref($data) eq 'ARRAY') {
153 42         68 Config::Model::Exception::User->throw(
154             object => $leaf_object,
155             error => "Cannot store twice the same value ('"
156 114     114   447684 .join("', '",@$data). "'). "
157 114 50       403 ."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       376 } ;
169 9 100       34  
170 9 50       37 my $unit_cb = sub {
171             my ($scanner, $data_ref,$node,@elements) = @_ ;
172 114         391  
173 42         292 # 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   3416035 next unless defined $unit_data;
177             $scanner->scan_element($unit_data, $node,$elt) ;
178             }
179 131         376 # read accepted elements
180 9801         152523 foreach my $elt (sort keys %$data_ref) {
181 9801 100       13735 my $unit_data = $data_ref->{$elt}; # extract relevant data
182 326         1104 $scanner->scan_element($unit_data, $node,$elt) ;
183             }
184             };
185 131         1548  
186 2         6 # this setup is required because IniFile backend cannot push value
187 2         7 # 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         181 # 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   16388 }
198 125 100       359  
199 125         297 };
200 125         6302  
201 131         4614 my $scan = Config::Model::ObjTreeScanner-> new (
202             node_content_cb => $unit_cb,
203             list_element_cb => $list_cb,
204 42         186 leaf_cb => $disp_leaf,
205             ) ;
206 42         399  
207             $scan->scan_node($data, $self->node) ;
208             return;
209             }
210              
211             # args are:
212 42         6746 # root => './my_test', # fake root directory, userd for tests
213 42         1847 # 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 2336850 # check => yes|no|skip
  20         95  
  20         89  
  20         42  
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       153 }
225 1         541 return 1;
226 1 50       4 }
227 1         163  
228 1         16 my ($unit_name, $unit_type) = $self->get_unit_info($args{file_path});
229 1         43  
230             my $app = $self->instance->application;
231 1         45 my $service_path;
232             if ($app =~ /-(user|file)$/) {
233             $service_path = $args{file_path};
234 19         18537  
235             $logger->debug("writing unit to $service_path");
236 19         57 # mouse super() does not work...
237 19         180 $self->SUPER::write(%args, file_path => $service_path);
238 19 100       100 }
239 10         27 else {
240             my $dir = $args{file_path}->parent->child("$unit_name.$unit_type.d");
241 10         44 $dir->mkpath({ mode => oct(755) });
242             $service_path = $dir->child('override.conf');
243 10         210  
244             $logger->debug("writing unit to $service_path");
245             # mouse super() does not work...
246 9         29 $self->SUPER::write(%args, file_path => $service_path);
247 9         566  
248 9         706 if (scalar $dir->children == 0) {
249             # remove empty dir
250 9         266 $logger->warn("Removing empty dir $dir");
251             rmdir $dir;
252 9         152 }
253             }
254 9 100       8261 return 1;
255             }
256 2         169  
257 2         29 my ($self, $args, $node, $elt) = @_ ;
258             # must skip disable element which cannot be hidden :-(
259             if ($elt eq 'disable') {
260 19         14462 return '';
261             } else {
262             return $self->SUPER::_write_leaf($args, $node, $elt);
263             }
264 4746     4746   2001611 }
265              
266 4746 100       7727 no Mouse ;
267 19         71 __PACKAGE__->meta->make_immutable ;
268              
269 4727         8475 1;
270              
271             # ABSTRACT: R/W backend for systemd unit files
272              
273 3     3   21  
  3         6  
  3         16  
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.251.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