Linux工具 Ansible

Linux工具 ansible
    Ansible是一个运维管理工具,可以减少一些重复的配置,比如有几百台主机需要进行相似的配置时或者对所有主机进行某些软件的版本升级时,如果是人工一台一台的配置是非常慢的,也容易出错,毕竟人精力有限;而这个Ansible运维工具就可以实现仅在一台主机上进行一遍配置,就可以实现配置所有主机的目的;
    运维工作:系统安装(物理机,虚拟机)→程序包安装、配置、服务启动→批量操作→程序发布→监控
    运维工作大致可分为三个层次:
        OS Provisioning:提供操作系统
            物理机:PXE,Cobbler
            虚拟机:Image Templates
        OS Configuration:提供配置文件
            puppet(ruby语言)
            saltstack(python语言)
            chef
            cfengine
        Command and Control:控制
            fabric
        预发布验证:
            一般新版本代码都会首先在公司内部的测试环境中运行,测试环境的配置和线上的配置一般都是相同的,只不过是没有加入到线上的调度器中;
        程序发布:使用的工具一般都是根据公司生产环境自研的
            注意事项:
                1.程序发布时不能影响用户体验
                2.系统不能停机
                3.不能导致系统故障或造成系统完全不可用
            一般线上发布都会采用灰度发布的形式:
                比如你有100台RS,可以先调度二十台主机下线,在这二十台RS上首先更新新版本,然后将其再上线,过一段时间如果没有出现事故,就接着批量升级剩下的RS主机;如果这二十台上线以后出现了故障,就需要马上下线然后进行软件版本的回滚,在将服务器上线,继续提供服务;
            回滚方式:可以在主机上同时存在多个版本,然后通过软连接的方式在各个版本中进行切换,从而实现回滚;
    运维工具的分类:
        agent:需要在被管理的主机上安装代理程序,然后才能与管控端进行通信;(puppet,func)
        agentless:无需在被管理的主机上安装代理程序,一般是基于ssh进行管理,所以一般会在管控端配置ssh基于秘钥的登录;(ansible,fabric)
    ansible特性:基于python研发的,由Paramiko(基于Python实现的ssh功能的API)、PyYAML(标记语言)和Jinja2(模板语言,用于实现playbooks)这三个主要模块组成;
        模块化的,可自定义模块
        部署简单
        支持主从模式
        支持playbook
        不用启动服务进程
 


    Ansible基础架构:
        Core Modules:实现最基本的系统配置、管理等功能;
        Custom Modules:自定义模块,Ansible支持使用市面上大多数编程语言开发的模块;
        Plugins:支持插件的方式完成功能扩展;
        Connection Plugins:连接插件,用于连接其管理的所有主机;
        Host Inventory:主机清单,所有被管理的主机都要记录到这个文件中;
        Playbooks:俗称“剧本”,可以实现自动化配置所有管理的主机;
    Ansible的使用:
        安装软件包:
            yum install ansible
        配置文件:
            /etc/ansible/ansible.cfg
            /etc/ansible/hosts
                此文件即为inventory文件,inventory文件遵循INI文件风格,”[ ]”中的字符为组名,可以将同一个主机归并到不同的组中,此外,若被管理的主机使用了非默认的SSH端口号,还可以在主机名之后使用”:”加端口来标明;
                例子:
                    [httpd]
                    www.nihao.com:12138
                    www.niyehao.con
                如果主机名的命名格式是按照某种规律来设置的,还可以使用列表的方式来进行批量设置;
                例子:
                    [nginx]
                    www.[1:9].guowei.com
                    Note:其中包含了www.1.guowei.com、www.2.guowei.com、…、www.9.guowi.com;生产中进行命名是建议命令方式可以见名知意;
                inventory中的主机变量:可以在inventory中定义主机时为其添加主机变量,便于在playbook中使用;
                    例子:
                        [DC]
                        www.superman.com http_port=80 maxRequestPerChild=12138
                inventory中的组变量:可以实现赋予组内的所有主机在playbook中可使用的变量;
                    例子:
                        [character]
                        www.spiderman.com
                        www.superman.com
                        [character:vars]
                        sex=man
                        hobby=unknown
                        Note:上面标示[character:vars]中的变量对[character]中的俩个主机都有效;其中[character:vars]是固定格式;
                inventory中的组嵌套:inventory中的组可以包含多个其他组;
                    例子:
                        [apache]
                        www.apache.com
                        [php]
                        www.php.com
                        [webserver:children]
                        apache
                        php
                        [webserver:vars]
                        ntp_server=ntp.api.bz
        配置ssh基于秘钥登录各个被管理主机
            ssh-keygen -t rsa
            ssh-copy-id -i id_rsa.pub root@node2
            ssh-copy-id -i id_rsa.pub root@clone1
            ssh-copy-id -i id_rsa.pub root@clone2
        vim /etc/ansible/hosts
            [websrvs]
            192.168.80.131
            192.168.80.134
            [dbsrvs]
            192.168.80.136
        测试:ansible  all  -a  'ifconfig'
        命令帮助:
            ansible <host-pattern> [options]
                host-pattern:可以使用/etc/ansible/hosts中指定的IP地址,也可以是[]中的段落名,还可以是all(表示所有主机);
                options:
                    -a args:指定模块的参数
                        args:一般为key=value格式;command模块要执行的命令格式无需为key=value格式,直接给出要执行的命令即可;
            ansible-doc -l:可以查看ansible支持的所有模块
            ansible-doc -s MODULE_NAME:查看特定模块的帮助信息
                常用模块:
                    command:直接执行命令即可
                    user:关于用户设置的模块
                        ansible-doc -s user
                        -a “name= state={present|ansent} system= uid= “
                        例子:ansible websrvs -m user -a "name=guowei state=present"
                                添加一个名为guowei的用户
                            ansible websrvs -m user -a "name=guowei state=absent"
                                删除刚才添加的名为guowei的用户
                    group:关于用户组设置的模块
                        ansible-doc -s group
                        -a “name= state={present|absent} gid= system= “
                    cron:关于周期性任务的模块
                        ansible-doc -s cron
                        -a “name=’ ‘ minute=’ ‘ job=’ ‘”
                        例子:ansible websrvs -m cron -a "name='sync time to all hosts' minute='*/5' job='/sbin/ntpdate ntp.api.bz &> /dev/null'"  →添加
                            ansible websrvs -m cron -a "name='sync time to all hosts' state='absent'"  →删除
                        我在执行上面的命令时出现了错误:"msg": "Aborting, target uses selinux but python bindings (libselinux-python) aren't installed!"
                        只需根据提示安装需要的软甲即可:yum install libselinux-python.x86_64 -y
                    copy:关于复制功能的模块
                        ansible-doc -s copy
                        -a “src= dest= mode= owner= “
                        例子:ansible websrvs -m copy -a "src=/etc/fstab dest=/tmp/fstab.bak mode=600"
                    file:关于设置文件属性的模块
                        ansible-doc -s file
                        -a “path= state= owner=”
                        例子:ansible websrvs -m file -a "path=/tmp/media state=directory"
                              ansible websrvs -m file -a "path=/tmp/ChangeLog-3.link state=link src=/tmp/ChangeLog-3"   →创建链接文件
                    ping:关于ping命令的模块
                        ansible-doc -s ping
                    service:关于服务管理的模块
                        ansible-doc -s service
                        -a “name= state={started|stopped|restarted} enabled= “
                        例子:ansible websrvs -m service -a "name=httpd state=started"
                              ansible websrvs -m service -a "name=httpd state=stopped"
                    yum:关于yum的模块
                        ansible-doc -s yum
                        -a “name= state={present|latest|absent} “
                        例子:ansible websrvs -m yum -a "name=/mnt/cdrom/Packages/httpd-2.2.15-69.el6.centos.x86_64.rpm"
                    shell:支持更强大的shell命令的模块
                        ansible-doc -s shell
                        -a “COMMAND”
                        例子:ansible websrvs -m shell -a "echo 'admin' | passwd --stdin guowei"
                    script:实现将本地脚本传输至远端节点然后执行的模块
                        ansible-doc -s script
                        -a “/path/to/script”
                    setup:收集远程主机的fact,类似可调用的变量;
                        ansible-doc -s setup
                        例子:ansible 192.168.80.131 -m setup
                            由于内容太多仅列出一小部分:
                            "ansible_date_time": {
                                "date": "2019-03-09",
                                "day": "09",
                                "epoch": "1552109574",
                                "hour": "13",
                                "iso8601": "2019-03-09T05:32:54Z",
                                "iso8601_basic": "20190309T133254819828",
                                "iso8601_basic_short": "20190309T133254",
                                "iso8601_micro": "2019-03-09T05:32:54.820139Z",
                                "minute": "32",
                                "month": "03",
                                "second": "54",
                            },  →格式为:{fact的名称:fact的值}
    Ansible基础元素:
        1.变量:只能由字母、数字、下划线组成,且开头只能为字母;
        2.facts:由正在通信的远程主机发回的信息,这些信息被保存在Ansible变量中;
        3.register:将任务的输出定义为变量,可供其他任务使用;
            例子:
                tasks:
                    - shell:/usr/bin/foo
                     register:foo_result
                     ignore_errors:True
        4.通过命令行传递变量:在运行playbook的时候也可以传递一些变量供playbook使用;例子:ansible-palybook test.yaml --extra-vars “hosts=localhost user=nginx”
        5.通过roles传递变量:当给一个主机应用角色的时候可以传递变量,然后再角色内使用这些变量    
    PlayBook:playbook是由一个或多个“play”组成的列表,可以理解成类似shell脚本一样的东西,其主要功能在于将事先归为一组的主机装扮成实现通过ansible中的tasks定义好的角色。从根本上讲,所谓的tasks无非是调用ansible的模块,从而完成一系列的操作。将多个play组织在一个playbook中,即可以将他们连同起来按事先编排的机制完成各自的任务(就像舞台上的每个角儿 都有自己的台词一样);
        例子:
            - hosts:webservers  指明下面的配置对哪个组生效
             vars:    设置变量
                http_port:80
                max_clients:256
             remote_user:root   指明被管理的主机上执行操作的用户
             tasks:  指明任务列表
                - name:add apache service
                 yum:name=httpd state=latest
                - name:start apache
                 service:name=httpd state=started
             handlers:
                - name:restart apache
                 service:name=httpd state=restarted
        内容:
            Inventory
                在/etc/asnibles/hosts中指定
            Modules
                指定使用到的模块
            Ad Hoc Commands
                指定使用到的命令
            PlayBooks
                Tasks:任务
                Variables:变量
                Templates:模板
                Handlers:处理器,由某事件触发以后才执行的操作
                Roles:角色
        YAML语言: 
            YAML是一个可读性高的用来表达资料序列的格式;
            特性:
                可读性好
                与脚本语言交互性好
                具有一致的信息模型
                易于实现
                基于流来处理
                扩展性好
            官网:https://yaml.org/spec/1.2/spec.html#id2708649
            语法:YAml的的语法和其他高阶语言类似,并且可以简单表达清单、散列表、标量等数据结构。其结构通过空格来展示,使用”-“来表示列表里的各项,MAP里的键值对使用”:”分隔;
                例子:
                name:books
                number:12138
                type:
                    - name:tianwen
                     number:12000
                    - name:dili
                     number:138
                Note:YAML文件扩展名通常为.yaml;一个键可以对应多个值,其中的值也可以是多个键值对的组合,类似于嵌套;
                列表(list):列表的所有元素均使用”-“开头;
                    - DC
                    - MCU
                    - TWDC
                字典(dictionary):通过key:value进行标识,由多个键值对组成;
                    name:MCU
                    character:Spider-Man
                    age:18(我猜的 d(゚∀゚d))
                    也可以写成:{ name:MCU,character:Spider-Man,age:18}
                例子:
                    vim test.yaml
                        tasks:
                            - name: install a package
                             yum:name=httpd state=latest
                            - name:add a user
                             user:name=guowei state=present
                            - name:start httpd service
                             service:name=httpd status=started
        Playbook中的基础组件:
            1.hosts和remote_user:
                playbook中的每一个play的目的都是为了让某个或某些主机以某个指定用户身份执行任务,hosts就是用于指定要执行的主机,其可以是一个或多个由”:”分隔的主机组;remote_user则用于指定被管理主机上的执行任务的用户,remote_user也可以用于各tasks中,因为各task中执行的操作可需要能使用不同用户来完成相关操作;关于用户的设置,也可以设置通过sudo的方式在远程主机上以普通用户的身份切换至root用户。从而完成相关操作;
                例子:
                    - hosts:webservers
                     remote_user:apache
                     tasks:
                        - name:add a user
                         user:name=superman state=present
                         remote_user:guowei
                         sudo:yes   指定由guowei切换到root然后添加superman这个用户;
            2.tasks和action
                play的主体部分是tasks list。tasks list中的各任务按次序逐个在hosts中指定的所有主机上执行,即在所有主机上完成第一个任务后再继续执行第二个任务,如果中途发生错误,可能会对某些已执行的任务进行回滚,因此在更正playboo以后重新执行一个即可,因为ansible的模块是幂等的,重复执行的结果是相同的,不会产生错误;tasks的目的是使用指定的参数执行模块,并且在模块的参数中可以使用变量;每个tasks都有”name”,用于playbook执行后显示相关提示信息,建议见名知意;定义tasks中的模块时,可以使用”action:module options”或”module:options”的格式,本文使用的一直都是后者;
                Note:在众多模块中只有command和shell模块仅需要给定一个列表,而无需使用”key=value”格式;
                      如果命令或脚本的退出状态码不为零,可以使用一下方式代替(当退出状态码不为零时,可能会影响playbook继续运行,所以有时需要这样设置,才能让剧本往下走)
                          tasks:
                              - name:run this comman and ignore the result
                             shell:/usr/bin/somecommand || /bin/true
                      也可以使用ignore_errors来忽略错误信息;
                          tasks:
                            - name:run this comman and ignore the result
                             shell:/usr/bin/somecommand
                             ignore_errors:True
            3.handlers
                用于当关注的资源发生变化时采取一定的操作;
                “notify”这个action可用于在每个play的最后被触发,这样可以避免多次有改变发生时每次都执行指定的操作,取而代之,仅在所有的变化发生完成后一次性地执行指定操作。在notify中列出的操作称为handler,也即notify中调用handler中定义的操作;
            例子:
            vim web_httpd.yaml
                - hosts: websrvs          指定作用的主机
                 remote_user: root       指定用户权限
                 tasks:
                 - name: copy local httpd.conf to remote hosts  复制httpd的配置文件
                  copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
                  notify:    当配置文件更新时,重启httpd服务,每个notify管理一个”- name”
                     - restart httpd
                 - name: start httpd service
                  service: name=httpd state=started enabled=true
                 handlers:   
                 - name: restart httpd    调用上面设置的notify,当notify指定的内容变化时,重启httpd;此处的名称需同上面notify中设置的名称保持一致;
                  service: name=httpd state=restarted
            ansible-playbook web_httpd.yaml
            4.调用变量
                直接上例子:可以通过vars定义变量,格式为”- var_name: value”,通过{{ var_name }}调用变量;也可以直接调用通过命令ansible host_name -m setup输出的变量;还可以在/etc/ansible/hosts中的主机名后面直接定义变量及其值;
                第一种方法:
                vim web_httpd.yaml
                    - hosts: websrvs
                      remote_user: root
                     vars:
                     - package: httpd
                     - service: httpd
                     tasks:
                       - name: install httpd pachage
                      yum: name= {{ package }} state=latest
                     - name: copy local httpd.conf to remote hosts
                      copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
                      notify:
                      - restart httpd
                     - name: start httpd service
                      service: name= {{ service }} state=started enabled=true
                      handlers:
                       - name: restart httpd
                       service: name=httpd state=restarted
                第二种方法:
                ansible 192.168.80.131 -m setup | less
                    "ansible_all_ipv4_addresses": [
                        "192.168.80.131", 
                        "10.101.1.133"
                    ],   由于内容太多仅列出一个,其格式为冒号前面的为变量名,中括号内的为变量值;
                    调用方法:同第一种方法类似,也是将变量名放在”{{ }}”中使用;同一模块参数同时调用多个变量时,需要使用引号将变量包含起来(content=” {{ var_name1 }}, {{ var_name2 }}” )
                第三种方法:
                vim /etc/ansible/hosts
                [webserver]
                192.168.80.131 var_name1=”var_value1”
                192.168.80.134 var_name2=”var_value2”
                    调用方法:同第一种方法类似,也是将变量名放在”{{ }}”中使用;
                    Note:每个主机后面的变量都是作用于自己的,所以就算变量名一样也没问题;
            5.条件测试:
                如果需要根据变量、facts或此前任务的执行结果来作为某tasks执行与否的前提时要用到条件测试;
                when语句
                    在tasks后添加when语句即可使用条件测试,when语句支持Jinja2表达式语法;
                    例子:
                        tasks:   当其操作系统为Centos时就进行关机操作;
                        - name: shutdown Centos systemc
                         command: /sbin/shutdown -h now
                         when: ansible_os _family == “Centos”
            6.迭代:
                当有需要重复性执行的任务时,可以使用迭代机制,其使用格式为将需要迭代的内容定义为item变量引用,并通过with_items语句来指明迭代的元素列表即可;
                例子:
                    - name: add user
                     user: name= {{ item }} state=present groups=testuser
                     with_items:
                     - testuser1
                     - testuser2
                上面的例子等同于:
                    - name: add user
                     user: name=testuser1 state=present groups=testuser
                    - name: add user
                     user: name=testuser2 state=present groups=testuser
                更为复杂一点的例子:可以实现类似子元素引用;
                    - name: add user
                     user: name= {{ item.name }} state=present groups= {{ item.groups }}
                       with_item:
                     - { name: ‘testuser1’, groups: ’testuser1’ }
                     - { name: ’testuser2’, groups: ’testuser2’ }
                    此例子实现的功能为:将用户testuser1添加到用户组testuser1中,将用户testuser2添加到用户组testuser2中;
                更多更详细的内容请参考官方文档:https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html?highlight=loops#loops   
            7.模板的使用:
                playbook可以通过将欲使用的某个服务的配置文件设置成自己的模板,然后通过使用各种自定义或者facts等提供的变量来替换其配置文件模板中的参数,来实现批量配置,并且还是根据每台被管理的主机的自身情况来进行配置的,这个功能实在是太好用了!使用的是JinJa2语言;
                例子:
                    复制一份httpd的配置文件到指定目录:
                        mkdir templates
                        cp httpd.conf templates/httpd.conf.jinja2
                        vim templates/httpd.conf.jinja2
                            Listen {{ http_port }}
                            ServerName {{ ansible_fqdn }}
                        vim /etc/ansible/hosts
                            [websrvs]
                            192.168.80.131  http_port=80
                            192.168.80.134  http_port=8080
                        vim web_httpd.yaml
                            - hosts: websrvs
                               remote_user: root
                               tasks:
                                - name: copy local httpd.conf to remote hosts
                                template: src=/root/templates/httpd.conf.jinja2 dest=/etc/httpd/conf/httpd.conf
                               notify:
                               - restart httpd
                             - name: start httpd service
                               service: name=httpd state=started enabled=true
                             handlers:
                              - name: restart httpd
                                service: name=httpd state=restarted
                        ansible-playbook web_httpd.yaml
            8.算数运算:
                Jinja2支持算数运算,所以我们可以通过调用数值变量来进行相关的运算;
                使用格式:{{ var_name1 {+|-|*|/|%|**|//} var_name2 }}
                    +:加
                    -:减
                    *:乘
                    /:除,商为浮点数
                    %:取余
                    **:幂
                    //:除,商为整数
            9.比较操作符:
                可以放在条件语句中,对变量的取值进行判断;
                ==:若二者相等,则为true
                !=:若二者不等,则为true
                >:若左边大于右边,则为true
                >=:若左边大于等于右边,则为true
                <:若左边小于右边,则为true
                <=:若左边小于等于右边,则为true
            10.Tags(标签)
                tags可以让playbook仅运行剧本中的部分代码,这样可以过跳过那些没有修改的,无需运行的代码,减少执行时间;
                例子:
                vim web_httpd.yaml
                            - hosts: websrvs
                               remote_user: root
                               tasks:
                                - name: copy local httpd.conf to remote hosts
                                template: src=/root/templates/httpd.conf.jinja2 dest=/etc/httpd/conf/httpd.conf
                              tags:
                              - conf
                               notify:
                               - restart httpd
                             - name: start httpd service
                               service: name=httpd state=started enabled=true
                             handlers:
                              - name: restart httpd
                                service: name=httpd state=restarted
                ansible-playbook web_httpd.yaml --tags=”conf”
                    当文件更改以后,可以通过上面的命令仅运行tags为conf的标签运行,也就是name为”copy local httpd.conf to remote hosts”的那个字典中所包含的内容;
            11.Roles
                roles能够根据层次性结构自动装载变量文件、tasks、以及handlers等,要使用roles只需要在playbook中使用include指令即可;简单来说,就是将变量、文件、r任务、模板以及处理器放置于单独的目录中,并可以便捷的include他们的一种机制,roles一般用于主机构件服务的场景中,但也是用于构建守护进程等场景中;
                roles的创建步骤:
                    创建以roles命令的目录;
                    在roles目录中分别创建以各角色名称命名的目录;
                    在每个roles命名的的目录中分别创建files、handlers、meta、tasks、templates、vars目录,用不到的目录可以为空或者根本就不创建;
                    在playbook文件中调用各roles
                roles内各子目录中的内容:
                    tasks目录:至少应该包含一个名为main.yml的文件,其定义可roles的任务列表;此文件可以使用include包含其他的位于此目录中的tasks文件;
                    files目录:存放由copy或script等模块调用的文件;一般为静态文件
                    templates目录:template模块会自定在此目录中寻找Jinja2模板文件;一般为包含变量的文件
                    handlers目录:此目录中应当包含一个main.yml文件,用于定义此roles用到的各handlers;在handlers中使用include包含的其他handlers文件,也应该位于此目录中;
                    vars目录:应当包含一个mail.yml文件,用于定义此角色用到的变量;
                    meta目录:应当包含一个mail.yml文件,用于定义此角色的特殊设定及其依赖关系;ansible1.3以后的版本才支持;
                    default目录:为当前角色设定默认变量时使用此目录,应当包含一个mail.yml文件;
                例子:
                    mkdir ansible_playbooks/roles/{websrvs,dbsrvs}/{tasks,handlers,vars,meta,files,templates} -pv
                    vim tasks/main.yml
                        - name: install httpd package
                         yum: name=httpd state=latest
                        - name: install config file
                         template: src=httpd.conf dest=/etc/httpd/conf/httpd.conf
                          tags:
                          - conf
                         notify:
                         - restart httpd
                        - name: start httpd service
                         service: name=httpd state=started
                    vim handlers/main.yml
                        - name: restart httpd
                          service: name=httpd state=restarted
                    cp httpd.conf ansible_playbooks/roles/websrvs/templates/
                    vim vars/main.yml
                        http_port: 8080
                    vim site.yml     此文件与roles在同一目录中
                        - hosts: websrvs
                           remote_user: root
                           roles:
                          - websrvs
                    ansible-playbook site.yml
                扩展:我们上面还创建了一个dbsrvs目录,我们可以将它作为mysql的playbook,设置过程跟上面类似,主要是在site.yml中的应用我们可以在roles中一起调用websrvs和dbsrvs或者根据不同的主机,分别调用websrvs和dbsrvs;
                    vim site.yml     此文件与roles在同一目录中
                        - hosts: 192.168.80.131
                           remote_user: root
                           roles:
                          - websrvs
                        - hosts: 192.168.80.134
                           remote_user: root
                           roles:
                          - dbsrvs
                        - hosts: 192.168.80.136
                           remote_user: root
                           roles:
                          - websrvs
                         - dbsrvs
        playbook的基本格式:
            - host:webservers
                remote_user:
                …
                tasks:
                    - module_name:module_args
                     …
                tasks:
                    …
            - host:dataservers
                …
                tasks:
                    - name:
                    …
            …
            例子:
                vim add_user.yaml
                    - hosts: websrvs
                       remote_user: root
                       tasks:
                     - name: create superman group
                      group: name=superman system=yes gid=357
                       - name: create superman user
                      user: name=superman uid=357 group=superman system=yes state=present
                    - hosts: dbsrvs
                       remote_user: root
                       tasks:
                       - name: copy file to dbsrvs
                      copy: src=/etc/fstab dest=/tmp/fstab.bak1
                ansible-playbook add_user.yaml
        命令格式:
            ansible-playbook [options] playbook.yml [playbook2 ...]
                --tags=’ ’:仅执行执行的tags标识的内容
 

        注:根据马哥视频做的学习笔记,如有错误,欢迎指正;侵删;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值