我通过伯乐在线翻译了一个Puppet简明教程,一共分为四部分,这是第三部分。

本文由 伯乐在线 - Wing 翻译,黄利民 校稿。未经许可,禁止转载!
英文出处:Manuel Kiessling。欢迎加入翻译组。

  • 《用 Puppet 搭建易管理的服务器基础架构(1)》
  • 《用 Puppet 搭建易管理的服务器基础架构(2)》

关于

在《用 Puppet 搭建易管理的服务器基础架构(2)》中,我们在 Puppet master上编写了第一个非常简单的清单,来对puppetclient虚拟机进行配置。现在我们要看一些更加复杂的配置,并且学习如何将清单组织成一个有用的结构。

模块

如果我们把服务器基础结构的那些可管理的配置一直存放在一个文件中,一旦当基础设施中的系统数目增加到一定程度时,就会不能很好扩展。对于一个基础结构来说,它的几个服务器要满足不同的角色,这样基础结构就会变得复杂。我们希望Puppet可以控制这种复杂局面。为了实现这些,我们需要保持清单中的信息整洁有序。

一种方式是将清单分解到不同的模块里。一个模块是一个或者多个清单的集合,这些清单放在一起,是因为它们有共同的用途。一个典型的模块示例是一个清单的集合,这些清单用来在目标机器上通过Apache HTTP服务器来管理web页面。这个模块会组装一些清单,这些清单关注不同的配置点:软件包安装、配置文件管理、服务管理。通过管理这些配置点,我们可以提供一个具有全部功能的web服务器。

我们现在要创建这样一个模块。一旦完成后,它会负责在puppetclient虚拟机上提供一个完整的、可操作的web服务器。

模块文件夹结构

从外部的角度来看,Puppet模块只是一些包含了特定文件的文件夹结构。如果你把正确的文件放到正确的文件夹结构中,那么这个结构中的清单可以被内部其他清单所引用。

我们来演示这一点,将在第二部分中创建的第一个简单清单中的相关部分移到模块中,然后我们会从主清单site.pp内部引用模块化的清单,而不是在site.pp内部直接创建清单声明。

当然,我们第一个清单只是一个”Hello World”示例,因此,我们将模块命名为helloworld。

为了做到这一点,我们只需要在puppetserver虚拟机上创建一个文件夹 /etc/puppet/modules/helloworld/manifests:

On the puppetserver VM

1
~# sudo mkdir -p /etc/puppet/modules/helloworld/manifests

除了清单本身,模块也可以在一个名为files的子目录下拥有文件,我们来创建这个目录,然后将helloworld.txt文件移动到这个目录下:

On the puppetserver VM

1
2
~# sudo mkdir -p /etc/puppet/modules/helloworld/files
~# sudo mv /etc/puppet/files/helloworld.txt /etc/puppet/modules/helloworld/files/

现在我们需要为新模块创建主清单文件,在/etc/puppet/modules/helloworld/manifests/init.pp中输入以下内容:

/etc/puppet/modules/helloworld/manifests/init.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
11
class helloworld {
  file { "/home/ubuntu/helloworld.txt":
    ensure => file,
    owner  => "ubuntu",
    group  => "ubuntu",
    mode   => 0644,
    source => "puppet://puppetserver/modules/helloworld/helloworld.txt"
  }
}

如你所见,这基本上是我们最初的file配置块,但是它现在被包在一个class块内,而且helloworld.txt的路径改变了。在Puppet清单中,class块可以用来定义配置块的地方,这样后期可以在其他地方使用这些配置块。file 块并不是 node 定义的一部分,虽然我们把它定义成 node 的一部分,但它还是在那里估算的。类本身并不是做任何事情,只有它在其它地方被声明时,才会有意义。我们可以通过including语句来在node块中声明我们新定义的类:

/etc/puppet/manifests/site.pp on puppetserver

1
2
3
4
5
node "puppetclient" {
  include helloworld
}

我们的节点定义现在不直接执行配置块,而只需要引用我们在helloworld类中定义的配置块,这样我们的节点也是新的helloworld模块中的一部分。将helloworld相关的配置都放到一个模块中,给我们带来了非常大的灵活性。设想一下我们有一个包含了成百上千个节点的集群,这些节点都需要接收helloworld配置块,我们可以通过复制-粘贴配置块的方式来实现,但这样在扩展方面不是特别好。然而,helloworld模块是可重用的,任意多数目的节点都可以使用它。

现在我们已经重新组织了现有的配置设置结构,它没有改变实际的配置目录,但是,当我们在puppetclient虚拟机上再次运行代理的话,可以看到:

On the puppetclient VM

1
2
3
4
5
~# sudo puppet agent --verbose --no-daemonize --onetime
info: Caching catalog for puppetclient
info: Applying configuration version '1396287576'
notice: Finished catalog run in 0.05 seconds

因为实际的状况和期望的配置是一致的,所以代理不会采取任何行动。

我们的新的清单在/etc/puppet目录下,新的目录结构如下所示。

1
2
3
4
5
6
7
8
9
10
11
files
manifests
  site.pp
modules
  helloworld
    manifests
      init.pp
    files
      helloworld.txt

第一个真实的模块:apache2

关于那些枯燥的Hello World示例,我已经谈了足够多的内容了,现在让我们在puppetclient客户端建立一个真实的web服务器吧。

我们首先来重命名helloworld模块, 因为我们已经不再需要它了。我们将会使用apache2 Ubuntu包来安装web服务器,这样我们也可以来调用新的apache2模块:

On the puppetserver VM

1
~# sudo mv /etc/puppet/modules/helloworld /etc/puppet/modules/apache2

一个包含了全部功能的apache2 Puppet模块需要处理下面几个方面:它必须保证某些软件包被安装了;它必须处理一些配置文件;它必须保证http守护进程服务在运行(当服务器的配置无论何时发生变化,这个进程都会被重启)。

一个好消息是Puppet模块在将来会被拆分成多个类。这样可以帮助我们很清晰的分离关注点。这种分离并不是Puppet本身所要求的,从长远来看,这样做可以让我们更容易的管理模块。

我们要谈论上一个模块中已有的init.pp清单文件,我们在/etc/puppet/modules/apache2/manifest目录下创建清单文件,文件名是install.pp。在这个文件里,我们会来处理一些软件包的安装工作。

/etc/puppet/modules/apache2/manifests/install.pp on puppetserver

1
2
3
4
5
6
7
class apache2::install {
  package { [ "apache2-mpm-prefork", "apache2-utils" ]:
    ensure => present
  }
}

我们这里会引入一个新的块类型:package。我们几乎可以它被使用的方式来读出它的目的:我们使用它来保证在目标系统中包括apache2-mpm-prefork包和apache-utils包。

另外,请注意对类进行命名的新语法,我们使用两个冒号来表示类结构中的命名空间。这个模块中的主类是apache2(我们会在init.pp清单文件中使用这个类名),其他的类会使用apache2名字空间。我们可以随便来为这些类命名——对于用来处理安装问题的清单来说,install可能是一个明智的选择。

就像之前解释的那样,Puppet为我们做了所有的重活,并利用apt-get来实现我们的期望,安装哪些有问题的软件包。

实际上,如果你想要的是一个运行的web服务器,那么安装这些软件包就足够了,但我们会再向前一步,我们会处理web服务器上的配置信息。

模板和变量

为了实现这一点,我们来创建一个新的类:apache::config,这个类位于/etc/puppet/modules/apache2/manifest/config.pp文件中:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
class apache2::config {
  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb")
  }
}

这看上去和我们之前遇到的file块有点儿像,但还是有一些明显的区别的。它包括了两个新内容:变量和模板(我们后面还会有模板中的变量)。

变量是一个强大的工具,我们可以用它来编写更加智能的清单。如你所见,文件路径通过hostname变量被参数化了,这个变量用来解析目标节点的完全限定域名字(full qualified domain name),对于我们的puppetclient虚拟机来说,主机名字当然就是puppetclient。

On the puppetclient VM

1
2
3
~# hostname
puppetclient

这以为这当我们在目标节点上运行清单文件时,它会创建一个名为/etc/apache2/sites-available/puppetclient.conf文件。

Puppet自己可以决定很多变量的值,hostname只是其中一个,稍后我们会在这个系列中看到如何定义我们自己的变量。

在file块中另外一个区别是content语句,它和我们之前遇到的source语句的不同之处在于,它不会指向一个静态文件,而是一个内嵌的ruby模板。对于静态文件,它只是从puppet master传输到客户端,而对于模板文件来说,它是动态的,可以包含变量和语句,这些变量会被替换成对应的值,而语句则会被执行。

我们可以在puppetserver虚拟机上查看一下/etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb文件,以便了解模板文件的结构:

/etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb on puppetserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<VirtualHost *:80>
  ServerName <%= hostname %>
  ServerAdmin webmaster@<%= hostname %>
  DocumentRoot /var/www
  <Directory />
    Options FollowSymLinks
    AllowOverride None
  </Directory>
  <Directory /var/www/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Directory>
  ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
  <Directory "/usr/lib/cgi-bin">
    AllowOverride None
    Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
    Order allow,deny
    Allow from all
  </Directory>
  ErrorLog ${APACHE_LOG_DIR}/<%= hostname %>.error.log
  LogLevel warn
  CustomLog ${APACHE_LOG_DIR}/<%= hostname %>.access.log combined
</VirtualHost>

如你所见,这个文件基本上就是你在平时常见的Apache虚拟机文件,但其中包含了一些占位符。在这里,我们只有一个占位符:<%=hostname%>。当我们将这个文件放到目标系统的目标文件位置后,文件中出现这个占位符的每个地方都会被替换成目标系统的完全限定域名字。

我们可以先试着运行一下新的清单,但目前还有一些不完美的地方需要我们首先做一些调整。对于Puppet清单来说,一个问题是你不能够真正预测清单块中各个应用程序的顺序。Puppet清单并不是批处理文件——它们并不是简单的按照从上到下的顺序执行。每一个块——就像文件或者包——都有自己的顺序。Puppet确实试着去决定一个有意义的执行顺序,但这不是很完美的。对我们来说,如果执行顺序很重要,那么我们需要帮助Puppet来实现这一点。

对于我们这个简单的apache2模块来说,在执行顺序方面,可能会有什么问题呢?好吧,我们安装了一个软件包,这个软件包在安装过程中会创建一个文件夹,我们要将一个文件放到这个文件夹中。如果Puppet在文件夹被创建出来之前(文件夹通过安装apache2-mpm-prefork包产生),就试着将虚拟机文件放到该文件夹中,那么就会发生一个错误。

我们可以在清单中向Puppet解释清单块之间的依赖关系。为了说明针对虚拟机文件的file模块依赖于apache2-mpm-prefork安装结束,我们可以向file块中添加require语句:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
11
class apache2::config {
  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"]
  }
}

请注意,当我们引用一个包时,Package语句中的P应该是大写的。

这样,Puppet就可以知道如何决定清单的执行顺序,它可以保证这样的规则:只有在apache2-mpm-prefork被安装到系统之后,才会执行file块。

第一次测试运行

现在关于apache2模块的install和config清单,我们有了第一个可以工作的版本。现在我们需要保证这个模块是可以应用到puppetclient虚拟机上的。为此,我们首先需要重写模块中的主清单:

/etc/puppet/modules/apache2/manifests/init.pp on puppetserver

1
2
3
4
5
6
class apache2 {
  include apache2::install
  include apache2::config
}

然后,我们需要将新模块映射到puppetclient节点的/etc/puppet/manifests/site.pp中:

/etc/puppet/manifests/site.pp on puppetserver

1
2
3
4
5
node "puppetclient" {
  include apache2
}

On the puppetclient VM

1
2
3
4
5
6
7
8
~# sudo puppet agent --verbose --no-daemonize --onetime
info: Caching catalog for puppetclient
info: Applying configuration version '1396980729'
notice: /Stage[main]/Apache2::Install/Package[apache2-utils]/ensure: ensure changed 'purged' to 'present'
notice: /Stage[main]/Apache2::Install/Package[apache2-mpm-prefork]/ensure: ensure changed 'purged' to 'present'
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/ensure: defined content as '{md5}c55c5bd945cea21c817bca1a465b7dd3'
notice: Finished catalog run in 16.62 seconds

好吧,现在可以工作了,但我们现在还没有实现目标。puppetclient虚拟机需要被激活。为了实现这一点,只需要需要一个从 /etc/apache2/sites-enabled/puppetclient.conf 到 /etc/apache2/sites-available/puppetclient.conf的符号链接,我们可以创建另外一个file类型块:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class apache2::config {
  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"]
  }
  file { "/etc/apache2/sites-enabled/${hostname}.conf":
    ensure  => link,
    target  => "/etc/apache2/sites-available/${hostname}.conf",
    require => File["/etc/apache2/sites-available/${hostname}.conf"]
  }
}

我们应用新的清单:

On the puppetclient VM

1
2
3
4
5
6
~# sudo puppet agent --verbose --no-daemonize --onetime
info: Caching catalog for puppetclient
info: Applying configuration version '1396983687'
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-enabled/puppetclient.conf]/ensure: created
notice: Finished catalog run in 0.07 seconds

这样我们就做完了,对吗?好吧,从某种程度上说,是的。我们安装了Apache,配置了虚拟主机。但是,如果使用Puppet,我们可以做得更好。如果手动管理puppetclient,我们会如何做呢?在修改了Apache配置后,我们会测试改动是否有任何错误(使用apachectl configtest),如果没有错误,我们会重启apache2服务。事实证明,Puppet也可以做到这一点。

改进模型

首先,我们需要让Puppet学习apache2服务。为此,我们需要在puppetserver虚拟机上创建另外一个清单文件,这个文件位于/etc/puppet/modules/apache2/manifests/service.pp

/etc/puppet/modules/apache2/manifests/service.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
11
12
class apache2::service {
  service { "apache2":
    ensure     => running,
    hasstatus  => true,
    hasrestart => true,
    restart    => "/usr/sbin/apachectl configtest && /usr/sbin/service apache2 reload",
    enable     => true,
    require    => Package["apache2-mpm-prefork"]
  }
}

如你所见,Puppet包括了service类型,可以用来处理守护(daemon)应用程序。在这个清单中,我们让Puppet来控制apache2服务。无论何时Puppet代理在目标系统运行,它都会确保这个服务是在运行。同时,Puppet还可以重启服务。在这种情况下,我们甚至可以教会Puppet只有在apachectl configtest运行没有错误时才会重启apache2服务。这样,我们就可以实现自动化的服务管理,而且没有放弃手动管理过程中的安全性检查。

当然,我们需要在主清单文件中声明一个新类:

/etc/puppet/modules/apache2/manifests/init.pp on puppetserver

1
2
3
4
5
6
class apache2 {
  include apache2::install
  include apache2::config
include apache2::service
}

最后,因为无论何时修改虚拟主机的配置,我们都想触发Apache重启动作,所以我们需要将config清单和service清单连在一起,为此实现这一点,我们可以添加一个notify语句:

/etc/puppet/modules/apache2/manifests/config.pp on puppetserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class apache2::config {
  file { "/etc/apache2/sites-available/${hostname}.conf":
    mode    => 0644,
    owner   => "root",
    group   => "root",
    content => template("apache2/etc/apache2/sites-available/vhost.conf.erb"),
    require => Package["apache2-mpm-prefork"],
    notify  => Service["apache2"]
  }
  file { "/etc/apache2/sites-enabled/${hostname}.conf":
    ensure  => link,
    target  => "/etc/apache2/sites-available/${hostname}.conf",
    require => File["/etc/apache2/sites-available/${hostname}.conf"]
  }
}

现在我们看一下是否可以按照我们期望的那样工作。可以简单的修改一下位于/etc/puppet/modules/apache2/templates/etc/apache2/sites-available/vhost.conf.erb的虚拟主机模板文件(添加一个空格或者空行就可以了),然后在puppetclient虚拟机上重新运行代理,我已经高亮显示了其中值得注意的地方:

On the puppetclient VM

1
2
3
4
5
6
7
8
9
10
~# sudo puppet agent --verbose --no-daemonize --onetime
info: Caching catalog for puppetclient
info: Applying configuration version '1396998784'
info: FileBucket adding {md5}c55c5bd945cea21c817bca1a465b7dd3
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Filebucketed /etc/apache2/sites-available/puppetclient.conf to puppet with sum c55c5bd945cea21c817bca1a465b7dd3
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/content: content changed '{md5}c55c5bd945cea21c817bca1a465b7dd3' to '{md5}afafea12b21e61c5e18879ce3fe475d2'
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Scheduling refresh of Service[apache2]
notice: /Stage[main]/Apache2::Service/Service[apache2]: Triggered 'refresh' from 1 events
notice: Finished catalog run in 0.32 seconds

我们也可以检查试下安全机制是否正常工作。再一次编辑虚拟机模板文件,让它变得不正确,例如,可以删掉一个标签的结束符>。

代理会像我们期望的那样处理:

On the puppetclient VM

1
2
3
4
5
6
7
8
9
10
~# sudo puppet agent --verbose --no-daemonize --onetime
info: Caching catalog for puppetclient
info: Applying configuration version '1396998943'
info: FileBucket adding {md5}d15f727106b64c29d1c188efc9e6c97d
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Filebucketed /etc/apache2/sites-available/puppetclient.conf to puppet with sum d15f727106b64c29d1c188efc9e6c97d
notice: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]/content: content changed '{md5}d15f727106b64c29d1c188efc9e6c97d' to '{md5}7c86c7ba3cd4d7522d36b9d9fdcd46e2'
info: /Stage[main]/Apache2::Config/File[/etc/apache2/sites-available/puppetclient.conf]: Scheduling refresh of Service[apache2]
err: /Stage[main]/Apache2::Service/Service[apache2]: Failed to call refresh: Could not restart Service[apache2]: Execution of '/usr/sbin/apachectl configtest && /usr/sbin/service apache2 reload' returned 1:  at /etc/puppet/modules/apache2/manifests/service.pp:10
notice: Finished catalog run in 0.19 seconds

不要忘记修改这个模板文件,这样才能正常工作。

结论和展望

在使用了模板、变量、依赖以及通知后,构建一个成熟的、健壮的清单是非常直接和简单的。使用模块以后,清单可以让更广阔的范围内的客户重用,在本系列的第4部分中,我们会讨论参数化如何让我们的模块更加灵活。

[翻译]用 Puppet 搭建易管理的服务器基础架构(3)相关推荐

  1. 华为桌面云服务器基础架构如何重启维护

    环境: FusionAccess 8.0.1 FusionCompute8.0.1 华为2288V5x2 问题描述: 华为桌面云服务器基础架构如何重启维护 解决方案: 1.进入FusionComput ...

  2. 搭建可视化管理DNS服务器

    DNS服务器搭建 环境配置 Centos7 #yum install bind bind-chroot #yum install  mysql-server php php-soap php-mysq ...

  3. 搭建易配置的分布式爬虫架构

    过年之后写的第一篇. 最近需要研究一下爬虫,这次的爬虫不是简单的requests+selenium+bs4或者是scrapy就能搞定的.因为要解决爬取多站点(200+)的问题,考虑到工作量的问题,所以 ...

  4. 内部管理类软件基础架构思想(思路图解 + 配套免费视频)

    两三年前就想把这个读取配置文件的,进行一次彻底得改造,一直没精力或者能力还不到位,说实话整体编写代码的能力不够的原因应该是占主要成分,由于这两三年,一直想把这个做好,所以想了很久了,思路成熟时,就一口 ...

  5. Nutanix推出云基础架构远程管理IT解决方案

    2020年6月30日,北京 -- 企业云计算领导者Nutanix(纳斯达克:NTNX)近日宣布,公司推出全新解决方案,助力IT团队无论在何地办公,都能随时对企业云基础架构进行部署.升级及故障排除.全新 ...

  6. 背后的力量 | 搭建新型IT基础架构 华云数据助力妇幼保健院提升数字化医院建设水平

    某市妇幼保健院创建于1953 年,是黔北地区唯一一所集临床医疗.科研 教学.预防保健.康复.基层卫生指导等功能为一体的三级妇幼保健院. 华云数据(微信号:chinac_com)是中国领先的综合云计算服 ...

  7. 什么是 IT 基础架构管理

    各行各业的企业组织不断面临创新和扩展的压力.就在十多年前,一个企业组织可以争取时间,在投资新技术方面保持保守,同时仍然保持竞争优势.快进到今天,随着业务实践的变化和新技术的不断涌现,商业和技术格局更加 ...

  8. 0. 服务器基础硬件维护方式

    0. 服务器基础硬件维护方式: 文章目录 0. 服务器基础硬件维护方式: 一. 管理方式 二. Dell Remote Access Controller (iDRAC) 三. OpenManage ...

  9. 必须了解的五个服务器基础问题

    必须了解的五个服务器基础问题 作者: 佚名 出处:IT世界  ( 0 ) 砖  ( 1 ) 好  评论 ( 0 ) 条 进入论坛 更新时间: 2007-12-10 11:27 关 键 词: 服务器 阅 ...

最新文章

  1. C#实现Winform自定义半透明遮罩层
  2. 牛顿迭代法求解平方根
  3. java 的类型转换方式
  4. 项目要开始,应该提出什么样的要求?
  5. 英文构词法 —— ant、ent 后缀
  6. s5p4418 Android 4.4.2 驱动层 HAL层 服务层 应用层 开发流程记录(二 硬件抽象层HAL 第二种 ioctl操作方法)
  7. 微信小程序获取unionid为空
  8. 关于H5跳转到小程序和android的方法
  9. 使用jedis访问redis
  10. 0x29——如何把自己iphone app传到iphone上
  11. Java经典设计模式-创建型模式-抽象工厂模式(Abstract Factory)
  12. 项目集成sentry
  13. java编程创建警告_java – 无法阻止ant生成编译器Sun专有API警告
  14. jq ajax异步上传文件,jQuery Ajax上传文件
  15. java fp-growth 算法包_java实现fp-growth算法
  16. 内存超频时序怎么调_超频讲解:内存时序设置一
  17. 线性代数标准型矩阵化简技巧
  18. 点阵发光管怎么用C语言编程,LED点阵经验各种点阵驱动方法讲解
  19. Django毕业设计题目推荐电影推荐系统
  20. (原创)关于中国象棋的

热门文章

  1. 加深认识与理解ADO.NET
  2. 关于VS 2008和.NET 3.5 Beta2新特性介绍
  3. Linux 一切皆文件认知
  4. 其他测试用例设计方法-错误推测法与正交实验法
  5. php 图片服务器搭建,php图像裁剪服务器搭建
  6. 为什么招聘高级前端开发这么难?
  7. 常见Web技术之间的关系,你知道多少?
  8. 双水泵轮换工作原理图_周宁气压给水设备控制柜原理图
  9. arm Linux 低成本方案,参赛作品《低成本基于ARM+Linux平台搭建web服务器的物联网学习板》...
  10. Redux-React 代码原理分析