提交 da13f2c7 编写于 作者: D Daniel P. Berrange

Add documentation for access control system

This adds two new pages to the website, acl.html describing
the general access control framework and permissions models,
and aclpolkit.html describing the use of polkit as an
access control driver.

page.xsl is modified to support a new syntax

  <div id="include" filename="somefile.htmlinc"/>

which will cause the XSL transform to replace that <div>
with the contents of 'somefile.htmlinc'. We use this in
the acl.html.in file, to pull the table of permissions
for each libvirt object. This table is autogenerated
from the enums in src/access/viraccessperms.h by the
genaclperms.pl script.

newapi.xsl is modified so that the list of permissions
checks shown against each API will link to the description
of the permissions in acl.html
Signed-off-by: NDaniel P. Berrange <berrange@redhat.com>
上级 0f3f0fad
...@@ -58,6 +58,7 @@ ...@@ -58,6 +58,7 @@
/daemon/libvirtd.policy /daemon/libvirtd.policy
/daemon/libvirtd.service /daemon/libvirtd.service
/daemon/test_libvirtd.aug /daemon/test_libvirtd.aug
/docs/aclperms.htmlinc
/docs/apibuild.py.stamp /docs/apibuild.py.stamp
/docs/devhelp/libvirt.devhelp /docs/devhelp/libvirt.devhelp
/docs/hvsupport.html.in /docs/hvsupport.html.in
......
...@@ -128,7 +128,7 @@ fig = \ ...@@ -128,7 +128,7 @@ fig = \
migration-unmanaged-direct.fig migration-unmanaged-direct.fig
EXTRA_DIST= \ EXTRA_DIST= \
apibuild.py \ apibuild.py genaclperms.pl \
site.xsl newapi.xsl news.xsl page.xsl \ site.xsl newapi.xsl news.xsl page.xsl \
hacking1.xsl hacking2.xsl wrapstring.xsl \ hacking1.xsl hacking2.xsl wrapstring.xsl \
$(dot_html) $(dot_html_in) $(gif) $(apihtml) $(apipng) \ $(dot_html) $(dot_html_in) $(gif) $(apihtml) $(apipng) \
...@@ -139,6 +139,16 @@ EXTRA_DIST= \ ...@@ -139,6 +139,16 @@ EXTRA_DIST= \
sitemap.html.in \ sitemap.html.in \
todo.pl hvsupport.pl todo.cfg-example todo.pl hvsupport.pl todo.cfg-example
BUILT_SOURCES += aclperms.htmlinc
CLEANFILES = $(srcdir)/aclperms.htmlinc
acl.html:: $(srcdir)/aclperms.htmlinc
$(srcdir)/aclperms.htmlinc: $(top_srcdir)/src/access/viraccessperm.h \
$(srcdir)/genaclperms.pl Makefile.am
$(PERL) $(srcdir)/genaclperms.pl $< > $@
MAINTAINERCLEANFILES = \ MAINTAINERCLEANFILES = \
$(addprefix $(srcdir)/,$(dot_html)) \ $(addprefix $(srcdir)/,$(dot_html)) \
$(addprefix $(srcdir)/,$(apihtml)) \ $(addprefix $(srcdir)/,$(apihtml)) \
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Client access control</h1>
<p>
Libvirt's client access control framework allows administrators
to setup fine grained permission rules across client users,
managed objects and API operations. This allows client connections
to be locked down to a minimal set of privileges.
</p>
<ul id="toc"></ul>
<h2><a name="intro">Access control introduction</a></h2>
<p>
In a default configuration, the libvirtd daemon has three levels
of access control. All connections start off in an unauthenticated
state, where the only API operations allowed are those required
to complete authentication. After successful authentication, a
connection either has full, unrestricted access to all libvirt
API calls, or is locked down to only "read only" operations,
according to what socket a client connection originated on.
</p>
<p>
The access control framework allows authenticated connections to
have fine grained permission rules to be defined by the administrator.
Every API call in libvirt has a set of permissions that will
be validated against the object being used. For example, the
<code>virDomainSetSchedulerParametersFlags</code> method will
check whether the client user has the <code>write</code>
permission on the <code>domain</code> object instance passed
in as a parameter. Further permissions will also be checked
if certain flags are set in the API call. In addition to
checks on the object passed in to an API call, some methods
will filter their results. For example the <code>virConnectListAllDomains</code>
method will check the <code>search_domains</code> on the <code>connect</code>
object, but will also filter the returned <code>domain</code>
objects to only those on which the client user has the
<code>getattr</code> permission.
</p>
<h2><a name="drivers">Access control drivers</a></h2>
<p>
The access control framework is designed as a pluggable
system to enable future integration with arbitrary access
control technologies. By default, the <code>none</code>
driver is used, which does no access control checks at
all. At this time, libvirt ships with support for using
<a href="http://www.freedesktop.org/wiki/Software/polkit/">polkit</a> as a real access
control driver. To learn how to use the polkit access
driver consult <a href="aclpolkit.html">the configuration
docs</a>.
</p>
<p>
The access driver is configured in the <code>libvirtd.conf</code>
configuration file, using the <code>access_drivers</code>
parameter. This parameter accepts an array of access control
driver names. If more than one access driver is requested,
then all must succeed in order for access to be granted.
To enable 'polkit' as the driver:
</p>
<pre>
# augtool -s set '/files/etc/libvirt/libvirtd.conf/access_drivers[1]' polkit
</pre>
<p>
And to reset back to the default (no-op) driver
</p>
<pre>
# augtool -s rm /files/etc/libvirt/libvirtd.conf/access_drivers
</pre>
<p>
<strong>Note:</strong> changes to libvirtd.conf require that
the libvirtd daemon be restarted.
</p>
<h2><a name="perms">Objects and permissions</a></h2>
<p>
Libvirt applies access control to all the main object
types in its API. Each object type, in turn, has a set
of permissions defined. To determine what permissions
are checked for specific API call, consult the
<a href="html/libvirt-libvirt.html">API reference manual</a>
documentation for the API in question.
</p>
<div id="include" filename="aclperms.htmlinc"/>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Polkit access control</h1>
<p>
Libvirt's client <a href="acl.html">access control framework</a> allows
administrators to setup fine grained permission rules across client users,
managed objects and API operations. This allows client connections
to be locked down to a minimal set of privileges. The polkit driver
provides a simple implementation of the access control framework.
</p>
<ul id="toc"></ul>
<h2><a name="intro">Introduction</a></h2>
<p>
A default install of libvirt will typically use
<a href="http://www.freedesktop.org/wiki/Software/polkit/">polkit</a>
to authenticate the initial user connection to libvirtd. This is a
very coarse grained check though, either allowing full read-write
access to all APIs, or just read-only access. The polkit access
control driver in libvirt builds on this capability to allow for
fine grained control over the operations a user may perform on an
object.
</p>
<h2><a name="perms">Permission names</a></h2>
<p>
The libvirt <a href="acl.html#perms">object names and permission names</a>
are mapped onto polkit action names using the simple pattern:
</p>
<pre>org.libvirt.api.$object.$permission
</pre>
<p>
The only caveat is that any underscore characters in the
object or permission names are converted to hyphens. So,
for example, the <code>search_storage_vols</code> permission
on the <code>storage_pool</code> object maps to the polkit
action:
</p>
<pre>org.libvirt.api.storage-pool.search-storage-vols
</pre>
<p>
The default policy for any permission which corresponds to
a "read only" operation, is to allow access. All other
permissions default to deny access.
</p>
<h2><a name="attrs">Object identity attributes</a></h2>
<p>
To allow polkit authorization rules to be written to match
against individual object instances, libvirt provides a number
of authorization detail attributes when performing a permission
check. The set of attributes varies according to the type
of object being checked
</p>
<h3><a name="object_connect">virConnectPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
</tbody>
</table>
<h3><a name="object_domain">virDomainPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>domain_name</td>
<td>Name of the domain, unique to the local host</td>
</tr>
<tr>
<td>domain_uuid</td>
<td>UUID of the domain, globally unique</td>
</tr>
</tbody>
</table>
<h3><a name="object_interface">virInterfacePtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>interface_name</td>
<td>Name of the network interface, unique to the local host</td>
</tr>
<tr>
<td>interface_mac</td>
<td>MAC address of the network interface, not unique</td>
</tr>
</tbody>
</table>
<h3><a name="object_network">virNetworkPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>network_name</td>
<td>Name of the network, unique to the local host</td>
</tr>
<tr>
<td>network_uuid</td>
<td>UUID of the network, globally unique</td>
</tr>
</tbody>
</table>
<h3><a name="object_node_device">virNodeDevicePtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>node_device_name</td>
<td>Name of the node device, unique to the local host</td>
</tr>
</tbody>
</table>
<h3><a name="object_nwfilter">virNWFilterPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>nwfilter_name</td>
<td>Name of the network filter, unique to the local host</td>
</tr>
<tr>
<td>nwfilter_uuid</td>
<td>UUID of the network filter, globally unique</td>
</tr>
</tbody>
</table>
<h3><a name="object_secret">virSecretPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>secret_uuid</td>
<td>UUID of the secret, globally unique</td>
</tr>
<tr>
<td>secret_usage_volume</td>
<td>Name of the associated volume, if any</td>
</tr>
<tr>
<td>secret_usage_ceph</td>
<td>Name of the associated Ceph server, if any</td>
</tr>
<tr>
<td>secret_usage_target</td>
<td>Name of the associated iSCSI target, if any</td>
</tr>
</tbody>
</table>
<h3><a name="object_storage_pool">virStoragePoolPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>pool_name</td>
<td>Name of the storage pool, unique to the local host</td>
</tr>
<tr>
<td>pool_uuid</td>
<td>UUID of the storage pool, globally unique</td>
</tr>
</tbody>
</table>
<h3><a name="object_storage_vol">virStorageVolPtr</a></h3>
<table class="acl">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect_driver</td>
<td>Name of the libvirt connection driver</td>
</tr>
<tr>
<td>pool_name</td>
<td>Name of the storage pool, unique to the local host</td>
</tr>
<tr>
<td>pool_uuid</td>
<td>UUID of the storage pool, globally unique</td>
</tr>
<tr>
<td>vol_name</td>
<td>Name of the storage volume, unique to the pool</td>
</tr>
<tr>
<td>vol_key</td>
<td>Key of the storage volume, globally unique</td>
</tr>
</tbody>
</table>
<h2><a name="user">User identity attributes</a></h2>
<p>
At this point in time, the only attribute provided by
libvirt to identify the user invoking the operation
is the PID of the client program. This means that the
polkit access control driver is only useful if connections
to libvirt are restricted to its UNIX domain socket. If
connections are being made to a TCP socket, no identifying
information is available and access will be denied.
Also note that if the client is connecting via an SSH
tunnel, it is the local SSH user that will be identified.
In future versions, it is expected that more information
about the client user will be provided, including the
SASL / Kerberos username and/or x509 distinguished
name obtained from the authentication provider in use.
</p>
<h2><a name="checks">Writing acces control policies</a></h2>
<p>
If using versions of polkit prior to 0.106 then it is only
possible to validate (user, permission) pairs via the <code>.pkla</code>
files. Fully validation of the (user, permission, object) triple
requires the new JavaScript <code>.rules</code> support that
was introduced in version 0.106. The latter is what will be
described here.
</p>
<p>
Libvirt does not ship any rules files by default. It merely
provides a definition of the default behaviour for each
action (permission). As noted earlier, permissions which
correspond to read-only operations in libvirt will be allowed
to all users by default; everything else is denied by default.
Defining custom rules requires creation of a file in the
<code>/etc/polkit-1/rules.d</code> directory with a name
chosen by the administrator (<code>100-libvirt-acl.rules</code>
would be a reasonable choice). See the <code>polkit(8)</code>
manual page for a description of how to write these files
in general. The key idea is to create a file containing
something like
</p>
<pre>
polkit.addRule(function(action, subject) {
....logic to check 'action' and 'subject'...
});
</pre>
<p>
In this code snippet above, the <code>action</code> object
instance will represent the libvirt permission being checked
along with identifying attributes for the object it is being
applied to. The <code>subject</code> meanwhile will identify
the libvirt client app (with the caveat above about it only
dealing with local clients connected via the UNIX socket).
On the <code>action</code> object, the permission name is
accessible via the <code>id</code> attribute, while the
object identifying attributes are exposed via a set of
attributes with the naming convention <code>_detail_[attrname]</code>.
For example, the 'domain_name' attribute would be exposed via
a property <code>_detail_domain_name</code>.
</p>
<h3><a name="exconnect">Example: restricting ability to connect to drivers</a></h3>
<p>
Consider a local user <code>berrange</code>
who has been granted permission to connect to libvirt in
full read-write mode. The goal is to only allow them to
use the <code>QEMU</code> driver and not the Xen or LXC
drivers which are also available in libvirtd.
To achieve this we need to write a rule which checks
whether the <code>_detail_connect_driver</code> attribute
is <code>QEMU</code>, and match on an action
name of <code>org.libvirt.api.connect.getattr</code>. Using
the javascript rules format, this ends up written as
</p>
<pre>
polkit.addRule(function(action, subject) {
if (action.id == "org.libvirt.api.connect.getattr" &amp;&amp;
subject.user == "berrange") {
if (action._detail_connect_driver == 'QEMU') {
return polkit.Result.YES;
} else {
return polkit.Result.NO;
}
}
});
</pre>
<h3><a name="exdomain">Example: restricting access to a single domain</a></h3>
<p>
Consider a local user <code>berrange</code>
who has been granted permission to connect to libvirt in
full read-write mode. The goal is to only allow them to
see the domain called <code>demo</code> on the LXC driver.
To achieve this we need to write a rule which checks
whether the <code>_detail_connect_driver</code> attribute
is <code>LXC</code> and the <code>_detail_domain_name</code>
attribute is <code>demo</code>, and match on a action
name of <code>org.libvirt.api.domain.getattr</code>. Using
the javascript rules format, this ends up written as
</p>
<pre>
polkit.addRule(function(action, subject) {
if (action.id == "org.libvirt.api.domain.getattr" &amp;&amp;
subject.user == "berrange") {
if (action._detail_connect_driver == 'LXC' &amp;&amp;
action._detail_domain_name == 'busy') {
return polkit.Result.YES;
} else {
return polkit.Result.NO;
}
}
});
</pre>
</body>
</html>
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<body> <body>
<h1 >Authentication &amp; access control</h1> <h1>Connection authentication</h1>
<p> <p>
When connecting to libvirt, some connections may require client When connecting to libvirt, some connections may require client
authentication before allowing use of the APIs. The set of possible authentication before allowing use of the APIs. The set of possible
authentication mechanisms is administrator controlled, independent authentication mechanisms is administrator controlled, independent
of applications using libvirt. of applications using libvirt. Once authenticated, libvirt can apply
fine grained <a href="acl.html">access control</a> to the operations
performed by a client.
</p> </p>
<ul id="toc"></ul> <ul id="toc"></ul>
......
#!/usr/bin/perl
#
# Copyright (C) 2013 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see
# <http://www.gnu.org/licenses/>.
#
use strict;
use warnings;
my @objects = (
"CONNECT", "DOMAIN", "INTERFACE",
"NETWORK","NODE_DEVICE", "NWFILTER",
"SECRET", "STORAGE_POOL", "STORAGE_VOL",
);
my %class;
foreach my $object (@objects) {
my $class = lc $object;
$class =~ s/(^\w|_\w)/uc $1/eg;
$class =~ s/_//g;
$class =~ s/Nwfilter/NWFilter/;
$class = "vir" . $class . "Ptr";
$class{$object} = $class;
}
my $objects = join ("|", @objects);
my %opts;
my $in_opts = 0;
my %perms;
while (<>) {
if ($in_opts) {
if (m,\*/,) {
$in_opts = 0;
} elsif (/\*\s*\@(\w+):\s*(.*?)\s*$/) {
$opts{$1} = $2;
}
} elsif (m,/\*\*,) {
$in_opts = 1;
} elsif (/VIR_ACCESS_PERM_($objects)_((?:\w|_)+),/) {
my $object = $1;
my $perm = lc $2;
next if $perm eq "last";
$perm =~ s/_/-/g;
$perms{$object} = {} unless exists $perms{$object};
$perms{$object}->{$perm} = {
desc => $opts{desc},
message => $opts{message},
anonymous => $opts{anonymous}
};
%opts = ();
}
}
print <<EOF;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
EOF
foreach my $object (sort { $a cmp $b } keys %perms) {
my $class = $class{$object};
my $olink = lc "object_" . $object;
print <<EOF;
<h3><a name="$olink">$class</a></h3>
<table class="acl">
<thead>
<tr>
<th>Permission</th>
<th>Description</th>
</tr>
</thead>
<tbody>
EOF
foreach my $perm (sort { $a cmp $b } keys %{$perms{$object}}) {
my $description = $perms{$object}->{$perm}->{desc};
die "missing description for $object.$perm" unless
defined $description;
my $plink = lc "perm_" . $object . "_" . $perm;
$plink =~ s/-/_/g;
print <<EOF;
<tr>
<td><a name="$plink">$perm</a></td>
<td>$description</td>
</tr>
EOF
}
print <<EOF;
</tbody>
</table>
EOF
}
print <<EOF;
</body>
</html>
EOF
...@@ -71,8 +71,8 @@ ...@@ -71,8 +71,8 @@
<xsl:template match="check" mode="acl"> <xsl:template match="check" mode="acl">
<tr> <tr>
<td><xsl:value-of select="@object"/></td> <td><a href="../acl.html#object_{@object}"><xsl:value-of select="@object"/></a></td>
<td><xsl:value-of select="@perm"/></td> <td><a href="../acl.html#perm_{@object}_{@perm}"><xsl:value-of select="@perm"/></a></td>
<xsl:choose> <xsl:choose>
<xsl:when test="@flags"> <xsl:when test="@flags">
<td><xsl:value-of select="@flags"/></td> <td><xsl:value-of select="@flags"/></td>
......
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
<xsl:call-template name="toc"/> <xsl:call-template name="toc"/>
</xsl:template> </xsl:template>
<xsl:template match="html:div[@id='include']" mode="content">
<xsl:call-template name="include"/>
</xsl:template>
<!-- This processes the sitemap to form a context sensitive <!-- This processes the sitemap to form a context sensitive
navigation menu for the current page --> navigation menu for the current page -->
<xsl:template match="html:ul" mode="menu"> <xsl:template match="html:ul" mode="menu">
...@@ -174,4 +178,11 @@ ...@@ -174,4 +178,11 @@
</html> </html>
</xsl:template> </xsl:template>
<xsl:template name="include">
<xsl:variable name="inchtml">
<xsl:copy-of select="document(@filename)"/>
</xsl:variable>
<xsl:apply-templates select="exsl:node-set($inchtml)/html:html/html:body/*" mode="content"/>
</xsl:template>
</xsl:stylesheet> </xsl:stylesheet>
...@@ -68,6 +68,16 @@ ...@@ -68,6 +68,16 @@
<a href="auth.html">Authentication</a> <a href="auth.html">Authentication</a>
<span>Configure authentication for the libvirt daemon</span> <span>Configure authentication for the libvirt daemon</span>
</li> </li>
<li>
<a href="acl.html">Access control</a>
<span>Configure access control libvirt APIs</span>
<ul>
<li>
<a href="aclpolkit.html">Polkit access control</a>
<span>Using polkit for API access control</span>
</li>
</ul>
</li>
<li> <li>
<a href="migration.html">Migration</a> <a href="migration.html">Migration</a>
<span>Migrating guests between machines</span> <span>Migrating guests between machines</span>
......
...@@ -1658,7 +1658,7 @@ test_libvirt_lockd.aug: locking/test_libvirt_lockd.aug.in \ ...@@ -1658,7 +1658,7 @@ test_libvirt_lockd.aug: locking/test_libvirt_lockd.aug.in \
test_virtlockd.aug: locking/test_virtlockd.aug.in \ test_virtlockd.aug: locking/test_virtlockd.aug.in \
locking/virtlockd.conf $(AUG_GENTEST) locking/virtlockd.conf $(AUG_GENTEST)
$(AM_V_GEN)$(AUG_GENTEST) locking/virtlockd.conf $< $@ $(AM_V_GEN)$(AUG_GENTEST) $(srcdir)/locking/virtlockd.conf $< $@
check-augeas-lockd: test_libvirt_lockd.aug check-augeas-lockd: test_libvirt_lockd.aug
$(AM_V_GEN)if test -x '$(AUGPARSE)'; then \ $(AM_V_GEN)if test -x '$(AUGPARSE)'; then \
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册