#!/usr/bin/perl
use
5.22.0;
use
open
qw(:std :encoding(UTF-8)
);
$Text::Wrap::columns
= 120;
no
warnings
qw/experimental::postderef experimental::signatures/
;
my
@service_list
=
qw/service socket timer/
;
my
@list
=
qw/exec kill resource-control unit/
;
my
$unknown_param_msg
=
"Unexpected systemd parameter. Please contact cme author to update systemd model."
;
my
%map
= (
'exec'
=>
'Common::Exec'
,
'kill'
=>
'Common::Kill'
,
'resource-control'
=>
'Common::ResourceControl'
,
);
my
%opt
;
GetOptions (\
%opt
,
"from=s"
) or
die
(
"Error in command line arguments\n"
);
die
"Missing '-from' option "
unless
$opt
{from};
my
(
$systemd_version
) = (`systemctl --version` =~ m/(\d+)/) ;
die
"Cannot find systemd version"
unless
$systemd_version
;
say
"Parsing man pages of systemd $systemd_version"
;
path(
'lib/Config/Model/models'
)->remove_tree;
my
$systemd_path
= path(
$opt
{from});
die
"Can't open directory "
.
$opt
{from}.
"\n"
unless
$systemd_path
->is_dir;
my
$systemd_man_path
=
$systemd_path
->child(
'man'
);
Config::Model::Exception::Trace(1);
sub
parse_xml (
$list
,
$map
) {
my
%data
= (
element
=> [] );
my
$config_class
;
my
$file
;
my
$subsystem
;
my
$desc
=
sub
(
$t
,
$elt
) {
my
$txt
=
$elt
->text;
$data
{class}{
$config_class
} //= [];
push
$data
{class}{
$config_class
}->@*,
$txt
;
};
my
$manpage
=
sub
(
$t
,
$elt
) {
my
$man
=
$elt
->first_child(
'refentrytitle'
)->text;
my
$nb
=
$elt
->first_child(
'manvolnum'
)->text;
$elt
->set_text(
qq!L<$man($nb)>!
);
};
my
$condition_variable
=
sub
(
$t
,
$elt
) {
my
@var_list
=
$elt
->children(
'term'
) ;
my
$listitem
=
$elt
->first_child(
'listitem'
);
my
$pre_doc
=
$listitem
->first_child_text(
'para'
);
my
$post_doc
=
$listitem
->last_child_text(
'para'
);
foreach
my
$var_elt
(
@var_list
) {
my
$var_name
=
$var_elt
->text;
my
(
$var_doc_elt
) =
$listitem
->get_xpath(
qq!./para/varname[string()="$var_name"]!
);
my
(
$name
,
$extra_info
) =
$var_name
=~ /C<([\w-]+)=([^>]*)>/ ;
die
"Error: cannot extract parameter name from '$var_name'"
unless
defined
$name
;
my
$desc
=
join
(
"\n\n"
,
$pre_doc
,
$var_doc_elt
->parent->text,
$post_doc
);
push
$data
{element}->@*, [
$config_class
=>
$name
=>
$desc
=>
$extra_info
];
}
};
my
$variable
=
sub
(
$t
,
$elt
) {
if
(
$systemd_version
< 244 and
$elt
->first_child_text(
'term'
) =~ /^C<Condition/) {
return
$condition_variable
->(
$t
,
$elt
);
}
my
@para_text
=
map
{
$_
->text}
$elt
->first_child(
'listitem'
)->children(
qr/para|programlisting/
);
my
$desc
=
join
(
"\n\n"
,
@para_text
);
my
@supersedes
;
if
(
$desc
=~ /settings? (?:are|is) deprecated. Use ([\w=\s,]+)./) {
my
$capture
= $1;
@supersedes
=
$capture
=~ /(\w+)=/g;
}
$desc
=~ s/C<([A-Z]\w+)=>(?!C<)/C<$1>/g;
$desc
=~ s/C<([A-Z]\w+)=>(?=C<)/C<$1>=/g;
$desc
=~ s/^\+-\+/ /gm;
$desc
=~ s/\n{3,}/\n\n/g;
foreach
my
$term_elt
(
$elt
->children(
'term'
)) {
my
$var_elt
=
$term_elt
->first_child(
'varname'
);
next
unless
$var_elt
;
my
$varname
=
$var_elt
->text;
my
(
$name
,
$extra_info
) =
$varname
=~ /C<([\w-]+)=([^>]*)>/ ;
next
unless
defined
$name
;
say
"- $config_class: storing parameter $name"
;
push
$data
{element}->@*, [
$config_class
=>
$name
=>
$desc
=>
$extra_info
=>
shift
@supersedes
];
}
};
my
$set_config_class
=
sub
(
$name
) {
$config_class
=
'Systemd::'
.(
$map
->{
$name
} ||
'Section::'
.
ucfirst
(
$name
));
say
"Parsing class $config_class from "
.
$file
->basename(
".xml"
) .
':'
;
};
my
$parse_sub_title
=
sub
{
my
$t
=
$_
->text();
if
(
$t
=~ /\[(\w+)\] Section Options/ ) {
$set_config_class
->($1) ;
}
};
my
$turn_to_pod_c
=
sub
{
my
$t
=
$_
->text();
return
if
$t
=~ /^C</;
$_
->set_text(
$t
=~ /[<>]/ ?
"C<< $t >>"
:
"C<$t>"
);
};
my
$twig
= XML::Twig->new (
twig_handlers
=> {
'refsect1/title'
=>
$parse_sub_title
,
'refsect1[string(title)=~ /Description/]/para'
=>
$desc
,
'refsect2/title'
=>
$parse_sub_title
,
'refsect2[string(title)=~ /Conditions/]/para'
=>
$desc
,
'citerefentry'
=>
$manpage
,
'literal'
=>
$turn_to_pod_c
,
'option'
=>
$turn_to_pod_c
,
'filename'
=>
$turn_to_pod_c
,
'constant'
=>
$turn_to_pod_c
,
ulink
=>
sub
{
my
$url
=
$_
->{att}{url};
my
$t
=
$_
->text();
$t
=~ s/^[\s\n]+//;
$t
=~ s/[\s\n]+$//;
$_
->set_text(
"L<$t|$url>"
);
},
'para'
=>
sub
{
$_
->subs_text(
qr/\n\s+/
,
"\n"
); 1;},
'programlisting'
=>
sub
{
my
$t
=
$_
->text();
$t
=~ s/\n\s*/\n+-+/g;
$_
->set_text(
"\n\n+-+$t\n\n"
);},
'varname'
=>
$turn_to_pod_c
,
'refsect1/variablelist/varlistentry'
=>
$variable
,
'refsect2/variablelist/varlistentry'
=>
$variable
,
}
);
foreach
my
$_subsystem
(
$list
->@*) {
$subsystem
=
$_subsystem
;
$file
=
$systemd_man_path
->child(
"systemd.$subsystem.xml"
);
$set_config_class
->(
$subsystem
);
$twig
->parsefile(
$file
);
}
return
\
%data
;
}
sub
check_for_list (
$element
,
$description
) {
my
$is_list
= 0;
$is_list
||=
$element
=~ /^(Exec|Condition)/ ;
$is_list
||=
$element
=~ /^(Requires|Requisite|Wants|BindsTo|PartOf|Conflicts)$/ ;
$is_list
||=
$element
=~ /^(DeviceAllow)$/ ;
$is_list
||=
$element
=~ /^Listen/ ;
$is_list
||=
$description
=~ /may be (specified|used) more than once/i ;
return
$is_list
?
qw/type=list cargo/
: () ;
}
sub
setup_element (
$meta_root
,
$config_class
,
$element
,
$desc
,
$extra_info
,
$supersedes
) {
my
@log
;
if
(not
$meta_root
->fetch_element(
'class'
)->
exists
(
$config_class
)) {
say
"Creating model class $config_class"
;
$meta_root
->load(
steps
=> [
qq!class:$config_class!
,
q!generated_by="parseman.pl from systemd doc"!
,
qq!accept:".*" type=leaf value_type=uniline warn="$unknown_param_msg"!
]);
}
my
$step
=
"class:$config_class element:$element"
;
my
$obj
=
$meta_root
->grab(
step
=>
$step
,
autoadd
=> 1);
$desc
=~ s/[\s\n]+/ /g;
my
$value_type
=
$desc
=~ /Takes a boolean (argument\s)?or/ ?
'enum'
:
$desc
=~ /Takes an? (boolean|integer)/ ? $1
:
$desc
=~ /Takes
time
\(in seconds\)/ ?
'integer'
:
$desc
=~ /allowed range/i ?
'integer'
:
$desc
=~ /Takes one of/ ?
'enum'
:
$desc
=~ /Takes the same
values
as/ ?
'enum'
:
$extra_info
=~ /\w\|\w/ ?
'enum'
:
'uniline'
;
if
(
$extra_info
and
$value_type
ne
'enum'
) {
push
@log
,
"did not use extra info: $extra_info"
unless
scalar
grep
{
$extra_info
eq
$_
}
qw/weight range/
;
}
my
(
$min
,
$max
);
if
(
$desc
=~ /Takes an integer between ([-\d]+) (?:\([\w\s]+\))? and ([-\d]+)/) {
(
$min
,
$max
) = ($1, $2);
push
@log
,
"integer between $min and $max"
;
}
if
(
$desc
=~ /allowed range is ([-\d]+) to ([-\d]+)/) {
(
$min
,
$max
) = ($1, $2);
push
@log
,
"integer range is $min to $max"
;
}
my
@load
;
my
@load_extra
;
if
(
$value_type
eq
'integer'
and
$desc
=~ /usual suffixes K/) {
$value_type
=
'uniline'
;
push
@load_extra
,
q!match="^\d+(?i)[KMG]$"!
;
}
push
@load
, check_for_list(
$element
,
$desc
);
push
@load
,
'type=leaf'
,
"value_type=$value_type"
;
push
@load_extra
,
'write_as=no,yes'
if
$value_type
eq
'boolean'
;
if
(
$value_type
eq
'enum'
) {
my
@choices
;
if
(
$desc
=~ /takes the same
values
as the (?:setting )?C<(\w+)>/i) {
my
$other
= $1;
my
$other_obj
=
$obj
->grab(
"- element:$other"
);
@choices
=
$other_obj
->fetch_element(
'choice'
)->fetch;
say
"Copy enum choices from $other to "
,
$obj
->location;
}
elsif
(
$extra_info
=~ /\w\|\w/) {
@choices
=
split
/\|/,
$extra_info
;
}
elsif
(
$desc
=~ /Takes a boolean (argument )?or /) {
my
(
$choices
) = (
$desc
=~ /Takes a boolean (?:argument )?or (?:the )?(?:special
values
|architecture identifiers\s*)?([^.]+?)\./);
@choices
= (
'no'
,
'yes'
);
push
@choices
, extract_choices(
$choices
);
push
@load
,
qw/replace:false=no replace:true=yes replace:0=no replace:1=yes/
;
}
if
(
$desc
=~ /Takes one of/) {
my
(
$choices
) = (
$desc
=~ /Takes one of ([^.]+?)(?:\.|to test)/);
@choices
= extract_choices(
$choices
);
}
die
"Error in $config_class: cannot find the values of $element enum type from «$desc»\n"
unless
@choices
;
push
@log
,
"enum choices are '"
.
join
(
"', '"
,
sort
@choices
).
"'"
;
push
@load_extra
,
'choice='
.
join
(
','
,
sort
@choices
);
}
push
@load_extra
,
"min=$min"
if
defined
$min
;
push
@load_extra
,
"max=$max"
if
defined
$max
;
if
(
$value_type
eq
'integer'
and
$desc
=~ /defaults? (?:value )?(?:to|is) (\d+)/i) {
push
@load_extra
,
"upstream_default=$1"
;
}
if
(
$supersedes
) {
push
@load_extra
,
"status=deprecated"
;
push
@log
,
"deprecated in favor of $supersedes"
;
my
$new
=
$meta_root
->grab(
step
=>
"class:$config_class element:$supersedes"
,
autoadd
=> 1
);
$new
->load(
steps
=>
qq!migrate_from variables:old="- $element" formula="\$old"!
);
}
$obj
->load(
step
=> [
@load
,
@load_extra
]);
say
"class $config_class element $element:\n\t"
.
join
(
"\n\t"
,
@log
)
if
@log
;
return
$obj
;
}
sub
extract_choices(
$choices
) {
my
@choices
= (
$choices
=~ m!C<([/\w\-+]+)>!g );
if
(
$choices
=~ m{possibly prefixed
with
(?:a )?C<([!\w]+)>} ) {
push
@choices
,
map
{
"$1$_"
}
@choices
;
}
return
@choices
;
}
sub
move_deprecated_element (
$meta_root
,
$from
,
$to
) {
say
"Handling move of service/$from to unit/$to..."
;
my
$warn
=
$from
eq
$to
?
"$from is now part of Unit."
:
"service/$from is now Unit/$to."
;
$meta_root
->load(
steps
=> [
'class:Systemd::Section::Service'
,
qq!element:$from type=leaf value_type=uniline status=deprecated!
,
qq!warn="$warn"!
]);
my
$from_element_dump
=
$meta_root
->grab(
"class:Systemd::Section::Unit element:$to"
)->dump_tree;
$meta_root
->load(
"class:Systemd::Section::Unit element:.rm($to)"
);
foreach
my
$service
(
@service_list
) {
my
$unit_class
=
"Systemd::Section::"
.
ucfirst
(
$service
).
'Unit'
;
$meta_root
->grab(
"class:$unit_class element:$to"
)
->load(
$from_element_dump
);
$meta_root
->load(
steps
=> [
"class:$unit_class include=Systemd::Section::Unit"
,
'accept:".*" type=leaf value_type=uniline warn="$unknown_param_msg"'
]);
}
$meta_root
->load(
steps
=> [
qq!class:Systemd::Section::ServiceUnit element:$to!
,
qq!migrate_from variables:service="- - Service $from" formula="\$service"!
]);
}
my
$data
= parse_xml([
@list
,
@service_list
], \
%map
) ;
my
$rw_obj
= Config::Model::Itself -> new () ;
$rw_obj
-> read_all() ;
my
$meta_root
=
$rw_obj
->meta_root;
foreach
my
$config_class
(
$meta_root
->fetch_element(
'class'
)->fetch_all_indexes) {
my
$gen
=
$meta_root
->grab_value(
step
=>
qq!class:$config_class generated_by!
,
mode
=>
'loose'
,
);
next
unless
$gen
and
$gen
=~ /parse-man/;
$meta_root
->load(
qq!class:-$config_class!
);
}
say
"Creating systemd model..."
;
foreach
my
$config_class
(
keys
$data
->{class}->%*) {
say
"Creating model class $config_class"
;
my
$desc_ref
=
$data
->{class}{
$config_class
};
my
$desc_text
=
join
(
"\n\n"
,
map
{ s/\n[\t ]+/\n/gr =~ s/C<([A-Z]\w+)=>/C<$1>/gr;}
$desc_ref
->@*);
$desc_text
.=
"\nThis configuration class was generated from systemd documentation.\n"
$desc_text
=~ s/^\+-\+/ /gm;
my
$steps
=
"class:$config_class class_description"
;
$meta_root
->grab(
step
=>
$steps
,
autoadd
=> 1)->store(
$desc_text
);
$meta_root
->load(
steps
=> [
qq!class:$config_class generated_by="parse-man.pl from systemd $systemd_version doc"!
,
qq!copyright:0="2010-2016 Lennart Poettering and others"!
,
qq!copyright:1="2016 Dominique Dumont"!
,
qq!license="LGPLv2.1+"!
,
qq!accept:".*" type=leaf value_type=uniline warn="$unknown_param_msg"!
,
]);
}
foreach
my
$cdata
(
$data
->{element}->@*) {
my
(
$config_class
,
$element
,
$desc
,
$extra_info
,
$supersedes
) =
$cdata
->@*;
my
$obj
= setup_element (
$meta_root
,
$config_class
,
$element
,
$desc
,
$extra_info
,
$supersedes
);
$desc
=~ s/ +$//gm;
$obj
->fetch_element(
"description"
)->store(wrap(
''
,
''
,
$desc
));
}
say
"Tweaking systemd model..."
;
$meta_root
->load(
'class:Systemd::Section::Service generated_by=
"parse-man.pl from systemd doc"
include:=Systemd::Common::ResourceControl,Systemd::Common::Exec,Systemd::Common::Kill'
);
$meta_root
->load(
'! class:Systemd::Common::Exec
element:IOSchedulingClass value_type=enum
choice=0,1,2,3,none,realtime,best-effort,idle'
);
my
$common_warp
=
qq!warp follow:disable="- disable" rules:\$disable level=hidden - - !
;
foreach
my
$service
(
@service_list
) {
my
$name
=
ucfirst
(
$service
);
my
$sub_class
=
'Systemd::'
.(
$map
{
$name
} ||
'Section::'
.
ucfirst
(
$name
));
my
$unit_class
=
$name
.
'Unit'
;
$meta_root
->load(
"class:Systemd::Section::$unit_class"
);
foreach
my
$class_name
(
"Systemd::$name"
,
"Systemd::StandAlone::$name"
) {
$meta_root
->load(
qq!
class:$class_name
generated_by="parse-man.pl from systemd doc"
accept:".*"
type=leaf
value_type=uniline
warn="$unknown_param_msg" - -!
);
}
$meta_root
->load(
qq!
class:Systemd::$name
element:disable
type=leaf
value_type=boolean
upstream_default=0
summary="disable configuration file supplied by the vendor"
description="When true, cme will disable a configuration file supplied by the vendor by placing place a symlink to /dev/null with the same filename as the vendor configuration file. See L<systemd-system.conf> for details." -
element:$name
type=warped_node
config_class_name=$sub_class
$common_warp -
element:Unit
type=warped_node
config_class_name=Systemd::Section::$unit_class
$common_warp -
element:Install
type=warped_node
config_class_name=Systemd::Section::Install
$common_warp -
rw_config
backend=Systemd::Unit
file=&index.$service
auto_delete=1
auto_create=1 !
);
$meta_root
->load(
qq!
class:Systemd::StandAlone::$name
element:$name
type=node
config_class_name=$sub_class -
element:Unit
type=node
config_class_name=Systemd::Section::$unit_class -
element:Install
type=node
config_class_name=Systemd::Section::Install -
rw_config
backend=Systemd::Unit
auto_delete=1
auto_create=1 !
);
$meta_root
->load(
qq!
class:Systemd
generated_by="parse-man.pl from systemd doc"
element:$service
type=hash
index_type=string
cargo
type=node
config_class_name=Systemd::$name - -
rw_config
backend=Systemd
auto_create=1
auto_delete=1 -
!
);
}
my
@moved
=
qw/FailureAction SuccessAction StartLimitBurst StartLimitInterval RebootArgument/
;
my
%move_target
=
qw/StartLimitInterval StartLimitIntervalSec/
;
foreach
my
$from
(
@moved
) {
my
$to
=
$move_target
{
$from
} ||
$from
;
move_deprecated_element(
$meta_root
,
$from
,
$to
);
}
say
"Handling move of StartLimitInterval to StartLimitIntervalSec in unit"
;
$meta_root
->load(
steps
=> [
'class:Systemd::Section::Unit'
,
qq!element:StartLimitInterval type=leaf value_type=uniline status=deprecated!
,
qq!warn="StartLimitInterval is now StartLimitIntervalSec."!
]);
$meta_root
->load(
steps
=> [
qq!class:Systemd::Section::ServiceUnit element:StartLimitIntervalSec!
,
qq!migrate_from variables:unit="- StartLimitInterval"!
,
q!use_eval=1 formula="$unit || $service"!
]);
say
"Handling move of OnFailureIsolate to OnFailureJobMode in unit"
;
$meta_root
->load(
steps
=> [
'class:Systemd::Section::Unit'
,
q!element:OnFailureIsolate type=leaf value_type=uniline status=deprecated!
,
q!warn="OnFailureIsolate is now OnFailureJobMode." -!
,
q!element:OnFailureJobMode!
,
q!migrate_from variables:unit="- OnFailureIsolate"!
,
q!formula="$unit"!
]);
say
"Saving systemd model..."
;
$rw_obj
->write_all;
say
"Done."
;