File Coverage

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