├── README.asciidoc ├── bootstrap-hosts ├── hosts ├── id_rsa.workshop ├── plays ├── ansible.yml ├── centos-install.yml ├── httpd.yml ├── library │ ├── lvol │ ├── qemu_img │ ├── virt_boot │ └── virt_guest ├── storage.yml └── vm.yml └── templates ├── centos-6.ks ├── etc ├── hosts ├── httpd │ └── conf.d │ │ ├── packages.conf │ │ └── packages.conf.v2 ├── ssh │ └── ssh_config └── yum.repos.d │ └── workshop.repo ├── root └── ssh │ └── id_rsa.workshop.pub └── vm.xml /README.asciidoc: -------------------------------------------------------------------------------- 1 | Ansible Workshop 2 | ================ 3 | 4 | Dag Wieers and Jeroen Hoekx 5 | v0.2, April 2013 6 | 7 | == Introduction == 8 | This workshop will construct a self-replicating virtual machine. 9 | 10 | We'll start with a short presentation as an introduction to Ansible. 11 | 12 | All content and code can be found at https://github.com/ansible-provisioning/workshop . 13 | 14 | === System Requirements === 15 | All examples assume a Linux system with KVM/Libvirt. The default libvirt network of +192.168.122.0/24+ is assumed to be present. 16 | 17 | Using LVM for virtual machine storage works out-of-the-box. File-based disks work, but require small changes in several files. 18 | 19 | == Getting Started == 20 | The virtual machine has the ip address +192.168.122.21+. The root password is root. SSH into it. 21 | 22 | We prefer to have a dedicated user to manage our Ansible setup. 23 | 24 | ---- 25 | [root@ws01 ~]# su - ansible 26 | [ansible@ws01 ~]$ ls 27 | workshop 28 | [ansible@ws01 ~]$ cd workshop/ 29 | [ansible@ws01 workshop]$ ls 30 | bootstrap-hosts hosts id_rsa.workshop plays templates 31 | ---- 32 | 33 | === Ad-Hoc Commands === 34 | The most straightforward usage of Ansible it to run commands on remote hosts. The +ping+ module checks connectivity and correct python setup. 35 | 36 | Let's assume our VM is remote and ping it. 37 | 38 | ---- 39 | [ansible@ws01 workshop]$ ansible ws01 -m ping -i hosts 40 | ws01 | success >> { 41 | "changed": false, 42 | "ping": "pong" 43 | } 44 | ---- 45 | 46 | The syntax is +ansible +. 47 | 48 | We only want to run it on the local host, so we choose +ws01+ as selector. The module is selected with the +-m+ switch. The +-i hosts+ switch tells Ansible which list of hosts to select from. 49 | 50 | Let's check what's in that file. 51 | 52 | ---- 53 | [ansible@ws01 workshop]$ cat hosts 54 | [guests] 55 | ws01 56 | ---- 57 | 58 | I think you know how to let it talk to a second machine. 59 | 60 | It really means that we have a group of systems +guests+ with just one system +ws01+ in it. 61 | 62 | We can of course also use Ansible to get the contents of the file. 63 | 64 | ---- 65 | [ansible@ws01 workshop]$ ansible ws01 -m command -a 'cat /home/ansible/workshop/hosts' -i hosts 66 | ws01 | success | rc=0 >> 67 | [guests] 68 | ws01 69 | ---- 70 | 71 | That uses the +command+ module and gives it arguments with +-a+. But avoid the 'hammer - nail' thing. 72 | 73 | == Installing Packages == 74 | Our goal is to create a self-replicating virtual machine. We will use Ansible to set up a CentOS 6 virtual machine on the VM host. We'll boot it and run Kickstart on it from a local package repository. The packages are found on the +ws01+ machine in +/srv/http/packages+. 75 | 76 | Apache will have to serve that directory. Let's install it. The +yum+ module installs packages on systems that have +yum+. As mentioned in the introduction, Ansible manages system state. That's clear from the arguments. 77 | 78 | ---- 79 | [ansible@ws01 workshop]$ ansible ws01 -m yum -a 'pkg=httpd state=installed' -i hosts 80 | ws01 | success >> { 81 | "changed": false, 82 | "msg": "", 83 | "rc": 0, 84 | "results": [ 85 | "httpd-2.2.15-26.el6.centos.x86_64 providing httpd is already installed" 86 | ] 87 | } 88 | ---- 89 | 90 | Apache was already installed. Let's install vim. 91 | 92 | ---- 93 | [ansible@ws01 workshop]$ ansible ws01 -m yum -a 'pkg=vim-enhanced state=installed' -i hosts 94 | ws01 | FAILED >> { 95 | "changed": false, 96 | "msg": "You need to be root to perform this command.\n", 97 | "rc": 1, 98 | "results": [ 99 | "" 100 | ] 101 | } 102 | ---- 103 | 104 | Try it again, this time as root user: 105 | 106 | ---- 107 | [ansible@ws01 workshop]$ ansible ws01 -m yum -a 'pkg=vim-enhanced state=installed' -i hosts -u root 108 | ws01 | success >> { 109 | "changed": true, 110 | "msg": "", 111 | "rc": 0, 112 | "results": [ 113 | "\n================================================================================\n Package Arch Version Repository Size\n================================================================================\nInstalling:\n vim-enhanced x86_64 2:7.2.411-1.8.el6 centos 892 k\nInstalling for dependencies:\n gpm-libs x86_64 1.20.6-12.el6 centos 28 k\n vim-common x86_64 2:7.2.411-1.8.el6 centos 6.0 M\n\nTransaction Summary\n================================================================================\nInstall 3 Package(s)\n\nTotal download size: 6.9 M\nInstalled size: 19 M\n\nInstalled:\n vim-enhanced.x86_64 2:7.2.411-1.8.el6 \n\nDependency Installed:\n gpm-libs.x86_64 0:1.20.6-12.el6 vim-common.x86_64 2:7.2.411-1.8.el6 \n\n" 114 | ] 115 | } 116 | ---- 117 | 118 | 119 | How does uninstalling a package work? 120 | 121 | ---- 122 | [ansible@ws01 workshop]$ ansible ws01 -m yum -a 'pkg=vim-enhanced state=absent' -i hosts -u root 123 | ws01 | success >> { 124 | "changed": true, 125 | "msg": "", 126 | "rc": 0, 127 | "results": [ 128 | "\n================================================================================\n Package Arch Version Repository Size\n================================================================================\nRemoving:\n vim-enhanced x86_64 2:7.2.411-1.8.el6 @centos 1.8 M\n\nTransaction Summary\n================================================================================\nRemove 1 Package(s)\n\nInstalled size: 1.8 M\n\nRemoved:\n vim-enhanced.x86_64 2:7.2.411-1.8.el6 \n\n" 129 | ] 130 | } 131 | ---- 132 | 133 | You can run the previous commands on many machines in parallel. This parallel ssh is already quite powerful. But it's not quite enough to create a self-replicating VM. 134 | 135 | == Playbooks == 136 | Playbooks are a powerful abstraction of system state. They contain a series of commands (tasks) with a slightly nicer syntax (YAML). 137 | 138 | Here's a minimal example that make sure Apache is on all guests. 139 | 140 | ---- 141 | --- 142 | 143 | - name: Configure the web server 144 | hosts: guests 145 | user: root 146 | 147 | tasks: 148 | - name: Install Apache 149 | action: yum pkg=httpd state=installed 150 | ---- 151 | 152 | There are two indentation levels. The outer one is called a play. The inner one is for tasks. Playbooks contain plays and plays contain tasks. A play also defines on which systems the tasks should be run. 153 | 154 | Save it as +plays/01-httpd.yml+. 155 | 156 | ---- 157 | [ansible@ws01 workshop]$ ansible-playbook plays/01-httpd.yml -i hosts 158 | 159 | PLAY [Configure the web server] ********************* 160 | 161 | GATHERING FACTS ********************* 162 | ok: [ws01] 163 | 164 | TASK: [Install Apache] ********************* 165 | ok: [ws01] 166 | 167 | PLAY RECAP ********************* 168 | ws01 : ok=2 changed=0 unreachable=0 failed=0 169 | ---- 170 | 171 | A play starts with a facts gathering phase. Variables like the operating system version or the mac addres of network interfaces will be available. 172 | 173 | Facts gathering is done by the +setup+ module. Run it to see which facts are available. 174 | 175 | ---- 176 | [ansible@ws01 workshop]$ ansible ws01 -m setup -i hosts -u root 177 | ... 178 | ---- 179 | 180 | === Templates === 181 | 182 | We can also define variables in a play by using the +vars+ keyword. The next example configures Apache to serve the packages dir. 183 | 184 | ---- 185 | --- 186 | 187 | - name: Configure the web server 188 | hosts: guests 189 | user: root 190 | 191 | vars: 192 | packages_path: /srv/http/packages 193 | 194 | tasks: 195 | - name: Install Apache 196 | action: yum pkg=httpd state=installed 197 | 198 | - name: Configure yum package location 199 | action: template src=../templates/etc/httpd/conf.d/packages.conf dest=/etc/httpd/conf.d/packages.conf 200 | 201 | - name: Start and enable Apache 202 | action: service name=httpd state=started enabled=yes 203 | ---- 204 | 205 | We encounter 2 new Ansible modules here. The +service+ module does what you expect it to do. It starts/stops/restarts and enables services on boot. 206 | 207 | The template module is more complicated. This allows you to use the variables and facts. A template is processed with jinja2, the same templating code used in the Flask Python web framework. 208 | 209 | Our template (in +templates/etc/http/conf.d/packages.conf+) looks like this: 210 | 211 | ---- 212 | Alias /packages {{ packages_path }} 213 | 214 | 215 | Options +Indexes 216 | Order allow,deny 217 | Allow from all 218 | 219 | ---- 220 | 221 | Variables can be defined in multiple places. You can add them to the inventory, create special variable files or define them in a play. 222 | 223 | Running the playbook results in: 224 | 225 | ---- 226 | [ansible@ws01 workshop]$ ansible-playbook plays/02-httpd.yml -i hosts 227 | 228 | PLAY [Configure the web server] ********************* 229 | 230 | GATHERING FACTS ********************* 231 | ok: [ws01] 232 | 233 | TASK: [Install Apache] ********************* 234 | ok: [ws01] 235 | 236 | TASK: [Configure yum package location] ********************* 237 | changed: [ws01] 238 | 239 | TASK: [Start and enable Apache] ********************* 240 | changed: [ws01] 241 | 242 | PLAY RECAP ********************* 243 | ws01 : ok=4 changed=2 unreachable=0 failed=0 244 | ---- 245 | 246 | Try to browse to the directory. 247 | 248 | Now, what happens when we run the playbook again? 249 | 250 | ---- 251 | [ansible@ws01 workshop]$ ansible-playbook plays/02-httpd.yml -i hosts 252 | 253 | PLAY [Configure the web server] ********************* 254 | 255 | GATHERING FACTS ********************* 256 | ok: [ws01] 257 | 258 | TASK: [Install Apache] ********************* 259 | ok: [ws01] 260 | 261 | TASK: [Configure yum package location] ********************* 262 | ok: [ws01] 263 | 264 | TASK: [Start and enable Apache] ********************* 265 | ok: [ws01] 266 | 267 | PLAY RECAP ********************* 268 | ws01 : ok=4 changed=0 unreachable=0 failed=0 269 | ---- 270 | 271 | Exactly nothing. 272 | 273 | That's because a playbook models system state. The state we want the system to be in did not change since our last run, so nothing gets changed. 274 | 275 | Ansible modules are ideally idempotent. This means you can run them as many times as possible and when your requested state does not change, nothing on the system will change. 276 | 277 | === Notify === 278 | 279 | Sometimes we are interested in state change and run actions when that happens. For example, when the package location configuration file for Apache changes, we want to restart Apache. 280 | 281 | An action to run when the state changes is a handler. This is just a task with another name. The same modules are available. Handlers are run at the end of the play, at least when they were notified of change. 282 | 283 | ---- 284 | --- 285 | 286 | - name: Configure the web server 287 | hosts: guests 288 | user: root 289 | 290 | vars: 291 | packages_path: /srv/http/packages 292 | 293 | handlers: 294 | - name: Restart Apache 295 | action: service name=httpd state=restarted 296 | 297 | tasks: 298 | - name: Install Apache 299 | action: yum pkg=httpd state=installed 300 | 301 | - name: Configure yum package location 302 | action: template src=../templates/etc/httpd/conf.d/packages.conf.v2 dest=/etc/httpd/conf.d/packages.conf 303 | notify: 304 | - Restart Apache 305 | 306 | - name: Start and enable Apache 307 | action: service name=httpd state=started enabled=yes 308 | ---- 309 | 310 | We've added a comment in the configuration file. Let's run that playbook. 311 | 312 | ---- 313 | [ansible@ws01 workshop]$ ansible-playbook plays/03-httpd.yml -i hosts 314 | 315 | PLAY [Configure the web server] ********************* 316 | 317 | GATHERING FACTS ********************* 318 | ok: [ws01] 319 | 320 | TASK: [Install Apache] ********************* 321 | ok: [ws01] 322 | 323 | TASK: [Configure yum package location] ********************* 324 | changed: [ws01] 325 | 326 | TASK: [Start and enable Apache] ********************* 327 | ok: [ws01] 328 | 329 | NOTIFIED: [Restart Apache] ********************* 330 | changed: [ws01] 331 | 332 | PLAY RECAP ********************* 333 | ws01 : ok=5 changed=2 unreachable=0 failed=0 334 | ---- 335 | 336 | == Advanced Inventory == 337 | 338 | Playbooks are only marginally useful when you run them on one machine. They become very powerful once you start managing multiple systems. 339 | 340 | Ansible does not do a name lookup when you specify you want to run something on 'ws01'. Ansible needs a host to be in the inventory file before it wants to talk to it. We've shown a very simple inventory file before: 341 | 342 | ---- 343 | [guests] 344 | ws01 345 | ---- 346 | 347 | Let's add the virtual machine host to it: 348 | 349 | ---- 350 | [ansible@ws01 workshop]$ cat hosts 351 | [hosts] 352 | 192.168.122.1 353 | 354 | [guests] 355 | ws01 356 | ---- 357 | 358 | Ansible uses SSH to talk to systems. The recommended way to login is to use public key authentication. 359 | 360 | Add the virtual machine public key to the hosts +/root/.ssh/authorized_keys+ file. 361 | 362 | ---- 363 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCa4iPbNVUYq7Ibkvj/9qI8CmSqRRCXQ/SAg9OA7Md/1UjSMELiMZsGu4A1LHpl4ER8nIet/w78p0amueIYgvX7oVY0+3fkXRqhJzqzoFVG8GzRZgpk9z8qX8aa3Dtq4rIGBH9st5hEcp3xkeap4+sv9xDd6X8Bd5gvYaCwvbU/vlgE6iYNpp45QNEaUOx50jHD3zPU6jShuJm/SnKmxW2HjXMY9DesYil5Dh2ixrYHoFjT1G/S1y+5plpTmylymd73oeu2cl04ImfT99Iufn7GAgjisSSDFC4o04jzm8bAzMKPf8/0iN1UrHmuR9rvmRqo3yWb7LTYdygSmqDOe5FB ansible@workshop 364 | ---- 365 | 366 | Try to login, you should not see any password prompt: 367 | 368 | ---- 369 | [ansible@ws01 workshop]$ ssh root@192.168.122.1 370 | Last login: Fri Mar 15 13:15:28 2013 from ws01 371 | [root@firefly ~]# 372 | ---- 373 | 374 | Now try to ping it with Ansible: 375 | 376 | ---- 377 | [ansible@ws01 workshop]$ ansible 192.168.122.1 -m ping -u root -i hosts 378 | 192.168.122.1 | success >> { 379 | "changed": false, 380 | "ping": "pong" 381 | } 382 | ---- 383 | 384 | We can now talk to multiple systems at once. The first argument to +ansible+ is not a system name, but a system selector. A magic value of 'all' will run a command on all systems. 385 | 386 | ---- 387 | [ansible@ws01 workshop]$ ansible all -m ping -u root -i hosts 388 | 192.168.122.1 | success >> { 389 | "changed": false, 390 | "ping": "pong" 391 | } 392 | 393 | ws01 | success >> { 394 | "changed": false, 395 | "ping": "pong" 396 | } 397 | ---- 398 | 399 | There are a lot of selectors you can choose from. The on-line documentation on them is excellent. 400 | 401 | == Orchestrate == 402 | 403 | We can now talk to the hypervisor. In order to deploy a virtual machine we first need to allocate storage. 404 | 405 | Using libvirt you have two options: 406 | 407 | - Use logical volumes 408 | - Use file based storage 409 | 410 | In production environments the LVM based approach is better, but for testing the file based storage might be good enough. 411 | 412 | There are Ansible modules for both. 413 | 414 | If you decide to go the LVM way you use the lvol module. There is one concept we have to introduce here. That's delegation of tasks. What we want to do is to create storage for the guest, but we want to do it on the hypervisor. 415 | 416 | The playbook looks like this: 417 | 418 | ---- 419 | --- 420 | 421 | - name: Allocate VM storage 422 | hosts: guests 423 | user: root 424 | gather_facts: no 425 | 426 | tasks: 427 | - action: lvol vg=${storage} lv=lv_${inventory_hostname}_root size=3072 428 | delegate_to: ${hypervisor} 429 | ---- 430 | 431 | You can see a few variables in here. They have to be defined in the inventory file. The +storage+ variable defines the volume group. The +hypervisor+ variable sets the system the storage has to be created on. The +inventory_hostname+ variable is Ansible magic. It's set to the name of the system in your inventory file. 432 | 433 | All variables are subsituted for every system in the selector. So they don't need to have the same values. 434 | 435 | Let's add the second VM we want to provision and define the variables: 436 | 437 | ---- 438 | [ansible@ws01 workshop]$ cat hosts 439 | [hosts] 440 | 192.168.122.1 441 | 442 | [guests] 443 | ws01 444 | ws02 445 | 446 | [guests:vars] 447 | hypervisor=192.168.122.1 448 | storage=firefly 449 | ---- 450 | 451 | This introduces group variables for the 'guests' group. 452 | 453 | We can run it: 454 | 455 | ---- 456 | [ansible@ws01 workshop]$ ansible-playbook plays/storage.yml --limit=ws02 -i hosts 457 | 458 | PLAY [Allocate VM storage] ********************* 459 | 460 | TASK: [lvol vg=${storage} lv=lv_${inventory_hostname}_root size=3072] ********************* 461 | changed: [ws02] 462 | 463 | PLAY RECAP ********************* 464 | ws02 : ok=1 changed=1 unreachable=0 failed=0 465 | ---- 466 | 467 | There's a new parameter to +ansible-playbook+. We've used +--limit=ws02+ to limit the playbook to system +ws02+. Any selector is valid here. 468 | 469 | If you use qemu storage, the playbook would be similar. 470 | 471 | ---- 472 | --- 473 | 474 | - name: Allocate VM storage 475 | hosts: guests 476 | user: root 477 | gather_facts: no 478 | 479 | tasks: 480 | - action: qemu_img dest=${qemu_img_path}/${inventory_hostname}.img size=3072 format=qcow2 481 | delegate_to: ${hypervisor} 482 | ---- 483 | 484 | In this case we would need to define the +qemu_img_path+ variable in the inventory. 485 | 486 | ---- 487 | [ansible@ws01 workshop]$ cat hosts 488 | [hosts] 489 | 192.168.122.1 490 | 491 | [guests] 492 | ws01 493 | ws02 494 | 495 | [guests:vars] 496 | hypervisor=192.168.122.1 497 | qemu_img_path=/var/lib/libvirt/images 498 | ---- 499 | 500 | And run the playbook: 501 | 502 | ---- 503 | [ansible@ws01 workshop]$ ansible-playbook plays/storage-qemu.yml --limit=ws02 -i hosts 504 | 505 | PLAY [Allocate VM storage] ********************* 506 | 507 | TASK: [qemu_img dest=${qemu_img_path}/${inventory_hostname}.img size=3072 format=qcow2] ********************* 508 | changed: [ws02] 509 | 510 | PLAY RECAP ********************* 511 | ws02 : ok=1 changed=1 unreachable=0 failed=0 512 | ---- 513 | 514 | === Conditionals === 515 | 516 | Now it's not really useful to have two separate playbooks. We want a way to have both methods in one playbook and choose which one to use depending on the variables that exist. 517 | 518 | Ansible has conditionals that allow you to do just that: 519 | 520 | ---- 521 | --- 522 | 523 | - name: Allocate VM storage 524 | hosts: guests 525 | user: root 526 | gather_facts: no 527 | 528 | tasks: 529 | - action: lvol vg=${storage} lv=lv_${inventory_hostname}_root size=3072 530 | delegate_to: ${hypervisor} 531 | when_set: ${storage} 532 | 533 | - action: qemu_img dest=${qemu_img_path}/${inventory_hostname}.img size=3072 format=qcow2 534 | delegate_to: ${hypervisor} 535 | when_set: ${qemu_img_path} 536 | ---- 537 | 538 | When we run it you will see that Ansible skipped the first one since only +qemu_img_path+ is defined. 539 | 540 | ---- 541 | ansible@ws01 workshop]$ ansible-playbook plays/storage.yml --limit=ws02 -i hosts 542 | 543 | PLAY [Allocate VM storage] ********************* 544 | 545 | TASK: [lvol vg=${storage} lv=lv_${inventory_hostname}_root size=3072] ********************* 546 | skipping: [ws02] 547 | 548 | TASK: [qemu_img dest=${qemu_img_path}/${inventory_hostname}.img size=3072 format=qcow2] ********************* 549 | ok: [ws02] 550 | 551 | PLAY RECAP ********************* 552 | ws02 : ok=1 changed=0 unreachable=0 failed=0 553 | ---- 554 | 555 | === Creating Virtual Machines === 556 | 557 | The next step is to actually create the virtual machine. Libvirt uses an XML description of it. We can just template that to the host and use the +virt_guest+ module to create the VM. 558 | 559 | ---- 560 | --- 561 | 562 | - name: Create the VM 563 | hosts: guests 564 | user: root 565 | gather_facts: no 566 | 567 | tasks: 568 | - action: file dest=/tmp/vm-${inventory_hostname} state=directory 569 | delegate_to: ${hypervisor} 570 | 571 | - action: template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml 572 | delegate_to: ${hypervisor} 573 | 574 | - action: virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml 575 | delegate_to: ${hypervisor} 576 | ---- 577 | 578 | We'll run this with verbose output: 579 | 580 | ---- 581 | [ansible@ws01 workshop]$ ansible-playbook plays/create-vm.yml --limit=ws02 -i hosts --verbose 582 | 583 | PLAY [Create the VM] ********************* 584 | 585 | TASK: [file dest=/tmp/vm-${inventory_hostname} state=directory] ********************* 586 | ok: [ws02] => {"changed": false, "group": "root", "mode": "0755", "owner": "root", "path": "/tmp/vm-ws02", "state": "directory"} 587 | 588 | TASK: [template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 589 | ok: [ws02] => {"changed": false, "dest": "/tmp/vm-ws02/vm.xml", "group": "root", "md5sum": "e59d86439a8db5f75f8fa9b9ba71694f", "mode": "0644", "owner": "root", "src": "/root/.ansible/tmp/ansible-1363682702.78-165223814933759/source", "state": "file"} 590 | 591 | TASK: [virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 592 | changed: [ws02] => {"changed": true, "provisioning_status": "unprovisioned"} 593 | 594 | PLAY RECAP ********************* 595 | ws02 : ok=3 changed=1 unreachable=0 failed=0 596 | ---- 597 | 598 | The verbose output shows the variables that modules return. This is stored in memory per system. 599 | 600 | In the next provisioning steps we don't want to reprovision an existing system. We can use the +provisioning_status+ variable returned by the +virt_guest+ module to limit our selection. We're not going to use the conditionals because that would look very ugly. The +group_by+ modules creates ad-hoc groups of systems. 601 | 602 | ---- 603 | --- 604 | 605 | - name: Create the VM 606 | hosts: guests 607 | user: root 608 | gather_facts: no 609 | 610 | tasks: 611 | - action: file dest=/tmp/vm-${inventory_hostname} state=directory 612 | delegate_to: ${hypervisor} 613 | 614 | - action: template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml 615 | delegate_to: ${hypervisor} 616 | 617 | - action: virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml 618 | delegate_to: ${hypervisor} 619 | register: guest 620 | 621 | - local_action: group_by key=${guest.provisioning_status} 622 | ---- 623 | 624 | Two new concepts here. The register keyword stores the output variables of a module in that variable. Group by creates a group with the given key. This is run on the command host. 625 | 626 | To use the new group, we add a second play to the playbook. This will create a kickstart file. 627 | 628 | ---- 629 | --- 630 | 631 | - name: Create the VM 632 | hosts: guests 633 | user: root 634 | gather_facts: no 635 | 636 | tasks: 637 | - action: file dest=/tmp/vm-${inventory_hostname} state=directory 638 | delegate_to: ${hypervisor} 639 | 640 | - action: template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml 641 | delegate_to: ${hypervisor} 642 | 643 | - action: virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml 644 | delegate_to: ${hypervisor} 645 | register: guest 646 | 647 | - local_action: group_by key=${guest.provisioning_status} 648 | 649 | - name: Install a minimal CentOS 650 | hosts: unprovisioned 651 | gather_facts: no 652 | user: root 653 | 654 | tasks: 655 | ### Prepare a kickstart file 656 | - local_action: file dest=${packages_path}/ks state=directory 657 | 658 | - local_action: template src=../templates/centos-6.ks dest=${packages_path}/ks/${inventory_hostname}.ks 659 | ---- 660 | 661 | We need to define a few more variables: 662 | 663 | ---- 664 | [ansible@ws01 workshop]$ cat hosts 665 | [hosts] 666 | 192.168.122.1 667 | 668 | [guests] 669 | ws01 ip=192.168.122.21 670 | ws02 ip=192.168.122.22 671 | 672 | [guests:vars] 673 | hypervisor=192.168.122.1 674 | master=192.168.122.21 675 | qemu_img_path=/var/lib/libvirt/images 676 | packages_path=/srv/http/packages 677 | packages_url=http://${master}/packages 678 | ---- 679 | 680 | Let's run it twice: 681 | 682 | ---- 683 | [ansible@ws01 workshop]$ ansible-playbook plays/provision.yml --limit=ws02 -i hosts 684 | 685 | PLAY [Create the VM] ********************* 686 | 687 | TASK: [file dest=/tmp/vm-${inventory_hostname} state=directory] ********************* 688 | ok: [ws02] 689 | 690 | TASK: [template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 691 | ok: [ws02] 692 | 693 | TASK: [virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 694 | changed: [ws02] 695 | 696 | TASK: [group_by key=${guest.provisioning_status}] ********************* 697 | changed: [ws02] 698 | 699 | PLAY [Install a minimal CentOS] ********************* 700 | 701 | TASK: [file dest=${packages_path}/ks state=directory] ********************* 702 | changed: [ws02] 703 | 704 | TASK: [template src=../templates/centos-6.ks dest=${packages_path}/ks/${inventory_hostname}.ks] ********************* 705 | changed: [ws02] 706 | 707 | PLAY RECAP ********************* 708 | ws02 : ok=6 changed=4 unreachable=0 failed=0 709 | 710 | 711 | [ansible@ws01 workshop]$ ansible-playbook plays/provision.yml --limit=ws02 -i hosts 712 | 713 | PLAY [Create the VM] ********************* 714 | 715 | TASK: [file dest=/tmp/vm-${inventory_hostname} state=directory] ********************* 716 | ok: [ws02] 717 | 718 | TASK: [template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 719 | ok: [ws02] 720 | 721 | TASK: [virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml] ********************* 722 | ok: [ws02] 723 | 724 | TASK: [group_by key=${guest.provisioning_status}] ********************* 725 | changed: [ws02] 726 | 727 | PLAY [Install a minimal CentOS] ********************* 728 | skipping: no hosts matched 729 | 730 | PLAY RECAP ********************* 731 | ws02 : ok=4 changed=1 unreachable=0 failed=0 732 | ---- 733 | -------------------------------------------------------------------------------- /bootstrap-hosts: -------------------------------------------------------------------------------- 1 | [hosts] 2 | 192.168.122.1 ansible_python_interpreter=/usr/bin/python2 3 | 4 | [guests] 5 | ws01 ip=192.168.122.21 6 | ws02 ip=192.168.122.22 7 | 8 | [guests:vars] 9 | master=192.168.122.21 10 | hypervisor=192.168.122.1 11 | qemu_img_path=/var/lib/libvirt/images 12 | packages_path=/srv/http/packages 13 | packages_url=http://${master}/packages 14 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | [guests] 2 | ws01 3 | -------------------------------------------------------------------------------- /id_rsa.workshop: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEAmuIj2zVVGKuyG5L4//aiPApkqkUQl0P0gIPTgOzHf9VI0jBC 3 | 4jGbBruANSx6ZeBEfJyHrf8O/KdGprniGIL1+6FWNPt35F0aoSc6s6BVRvBs0WYK 4 | ZPc/Kl/Gmtw7auKyBgR/bLeYRHKd8ZHmqePrL/cQ3el/AXeYL2GgsL21P75YBOom 5 | DaaeOUDRGlDsedIxw98z1Oo0obiZv0pypsVth41zGPQ3rGIpeQ4dosa2B6BY09Rv 6 | 0tcvuaZaU5spcpne96HrtnJdOCJn0/fSLn5+xgII4rEkgxQuKNOI85vGwMzCj3/P 7 | 9IjdVKx5rkfa75kaqN8lm+y02HcoEpqgznuRQQIDAQABAoIBAQCNCWwZSyFoS8Du 8 | NxGjE9V70wMDwcxv0iOte113wyWPlIqxS9072GwQ32DKCuySJHx49JjgqqfdDf3a 9 | CN6H74lLUAkOSgdM3jNHmE9uDoxZAso0jDTe5/6O+ZQCpJU+qZvuut3GBBEWE0Ec 10 | Hv3qqm8ZyGOFkABlN6BPVRlcmAOaPLF+2l/DA9S0gXwCM4B0RvWq1TCFSTFz2RsV 11 | OIaGYxEtCXo8wTD7Jr0cKZSYkBVTrj6Vm0DM7ZBRC1OsC+JxtUwE+d425foTPqSs 12 | h9I8kPKF9DjIXEyVqgMbm6yxQ+5Z2JRzn4E7JUEy4NQe8igcscqHW9RAGty6s96G 13 | 8dLJ3VehAoGBAMnsgghMIgDDKYA+pDvxTh23NDDY0/JIZnpIWS32DHu4hLsFFneS 14 | 43h95O7U7TrXaUJQC6UOuPb7pfSk0mj61JhJHOsezIdS6L0hxmdXvK0vc5ngWG/0 15 | zHuOchuoSpkG9m6SALZPTEQ9MdmWRJcaID0Rlnm8H2PUZDtXG9grdbKtAoGBAMRc 16 | oWXpPgZyF59cWaNu+kbQhd2gyVVUolGk3GeEx9cZxlfx18WN+nBq3IOnTKsa7Eq0 17 | KgU3cvRuO2cv1Bd257tnKCJQioXwYBgak4CGoRVuES4e79pJvKPL8uf3d9sosif6 18 | JpVFf1OxTGvP4n7sypIAcJNjk5LdT0rjN1yQUb9lAoGBAKgf4xjTgxBNbvWXspky 19 | To9RZgQx1S8K90BjktVA453zwZgSIWXICNvfPslYwnlWuA59pWR2AK2sU76Bqau9 20 | BVwSrCBcUYFvF9e6En8jPzaXptH9SMVW9xb9QKcklZAaiv7/U9Z36hF7PlFj25JQ 21 | L32JclfDugMd6aK64bU4YlQ1AoGBAI0sw+VPUga0VIOAk/nKuinblcMH7HhrBuCI 22 | FOZgMoVVxKJKAAXK0/mq+qu0xoxmKOh0q5lgikduUUsYufW8yVKVEefJ3C376jqq 23 | MM5A/OM5ZSSxnWRliziAUz2vT/7DPYM8eCzt8GMtn3IL3h2/BMz/f/CXsOvwLSf8 24 | QDtOj1d5AoGBALCAQBbF8yI3hhhxVuKnhf5H0DN1ZffbaUfe35XaXDkVRxXaJaPE 25 | J/x63palrFQrw88I0Iwj7duE1QcyrnGwa+1ZaRu7893KQTYH3mxmslvPD83mXZjq 26 | CEClcakcfm//Zdd6PB9aQwiyXXcwn83U0H7ZPlNmwb5Nw03FReoZj4Ui 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /plays/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Configure Ansible 4 | hosts: guests 5 | user: root 6 | tasks: 7 | 8 | - name: Install Ansible 9 | action: yum pkg=$item state=installed 10 | with_items: 11 | - ansible 12 | - git 13 | 14 | - name: Create an Ansible user 15 | action: user name=ansible 16 | 17 | - name: Install our key as authorized 18 | action: authorized_key user=ansible key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCa4iPbNVUYq7Ibkvj/9qI8CmSqRRCXQ/SAg9OA7Md/1UjSMELiMZsGu4A1LHpl4ER8nIet/w78p0amueIYgvX7oVY0+3fkXRqhJzqzoFVG8GzRZgpk9z8qX8aa3Dtq4rIGBH9st5hEcp3xkeap4+sv9xDd6X8Bd5gvYaCwvbU/vlgE6iYNpp45QNEaUOx50jHD3zPU6jShuJm/SnKmxW2HjXMY9DesYil5Dh2ixrYHoFjT1G/S1y+5plpTmylymd73oeu2cl04ImfT99Iufn7GAgjisSSDFC4o04jzm8bAzMKPf8/0iN1UrHmuR9rvmRqo3yWb7LTYdygSmqDOe5FB ansible@workshop" 19 | 20 | - name: Remove default Ansible hosts file 21 | action: file dest=/etc/ansible/hosts state=absent 22 | 23 | - name: Configure hosts file to talk to other guests 24 | action: template src=../templates/etc/hosts dest=/etc/hosts 25 | 26 | - name: Disable strict host key checking for the demo 27 | action: copy src=../templates/etc/ssh/ssh_config dest=/etc/ssh/ssh_config 28 | 29 | - name: Create Ansible environment 30 | hosts: guests 31 | user: ansible 32 | 33 | tasks: 34 | - name: Prepare SSH directory 35 | action: file dest=/home/ansible/.ssh state=directory mode=0700 36 | 37 | - name: Copy private key 38 | action: copy src=../id_rsa.workshop dest=/home/ansible/.ssh/id_rsa mode=0600 39 | 40 | - name: Copy public key 41 | action: copy src=../templates/root/ssh/id_rsa.workshop.pub dest=/home/ansible/.ssh/id_rsa.pub 42 | 43 | - name: Prepare Ansible directory 44 | action: file dest=/home/ansible/workshop state=directory 45 | 46 | - name: Clone git repo from command host 47 | action: git repo=ansible@${master}:workshop/ dest=/home/ansible/workshop 48 | 49 | - name: Correct private key permissions 50 | action: file dest=/home/ansible/workshop/id_rsa.workshop mode=0600 51 | -------------------------------------------------------------------------------- /plays/centos-install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include: storage.yml 4 | - include: vm.yml 5 | 6 | - name: Install a minimal CentOS 7 | hosts: unprovisioned 8 | gather_facts: no 9 | user: root 10 | 11 | tasks: 12 | ### Prepare a kickstart file 13 | - action: file dest=${packages_path}/ks state=directory 14 | delegate_to: ${master} 15 | 16 | - action: template src=../templates/centos-6.ks dest=${packages_path}/ks/${inventory_hostname}.ks 17 | delegate_to: ${master} 18 | 19 | - name: Copy kernel and initrd to host 20 | action: get_url url=${packages_url}/minimal/isolinux/$item dest=/tmp/vm-${inventory_hostname}/$item 21 | delegate_to: ${hypervisor} 22 | with_items: 23 | - vmlinuz 24 | - initrd.img 25 | 26 | ### Boot the VM using the ISO image 27 | - action: virt_boot guest=${inventory_hostname} kernel=/tmp/vm-${inventory_hostname}/vmlinuz initrd=/tmp/vm-${inventory_hostname}/initrd.img cmdline="linux sshd ksdevice=eth0 ip=${ip} netmask=255.255.255.0 gateway=192.168.122.1 ks=${packages_url}/ks/${inventory_hostname}.ks" 28 | delegate_to: ${hypervisor} 29 | 30 | ### Wait for kickstart to finish 31 | - local_action: wait_for host=${ip} port=22 state=started 32 | - local_action: wait_for host=${ip} port=22 state=stopped timeout=300 33 | - local_action: pause seconds=15 34 | 35 | - name: Start the Virtual Machines 36 | hosts: guests 37 | user: root 38 | 39 | ### Can't gather facts from a machine that does not exist 40 | gather_facts: no 41 | 42 | tasks: 43 | - action: virt_boot guest=${inventory_hostname} boot=hd 44 | delegate_to: ${hypervisor} 45 | tags: 46 | - post_install 47 | 48 | - local_action: wait_for host=${ip} port=22 state=started 49 | tags: 50 | - post_install 51 | 52 | - action: lineinfile dest=/etc/hosts regexp=" ${inventory_hostname}$" line="${ip} ${inventory_hostname}" 53 | delegate_to: ${master} 54 | tags: 55 | - post_install 56 | 57 | - name: Configure yum on the newly provisioned machine 58 | action: template src=../templates/etc/yum.repos.d/workshop.repo dest=/etc/yum.repos.d/workshop.repo 59 | tags: 60 | - post_install 61 | 62 | - name: Remove default CentOS repos 63 | action: file dest=/etc/yum.repos.d/$item state=absent 64 | with_items: 65 | - CentOS-Base.repo 66 | - CentOS-Vault.repo 67 | - epel.repo 68 | - epel-testing.repo 69 | tags: 70 | - post_install 71 | -------------------------------------------------------------------------------- /plays/httpd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Configure the web server 4 | hosts: guests 5 | user: root 6 | vars: 7 | packages_path: /srv/http/packages 8 | centos_image: /srv/http/packages/CentOS-6.4-i386-minimal.iso 9 | 10 | handlers: 11 | - name: Restart Apache 12 | action: service name=httpd state=restarted 13 | 14 | tasks: 15 | 16 | - name: Install Apache 17 | action: yum pkg=httpd state=installed 18 | 19 | - name: Configure yum package location 20 | action: template src=../templates/etc/httpd/conf.d/packages.conf dest=/etc/httpd/conf.d/packages.conf 21 | notify: 22 | - Restart Apache 23 | 24 | - name: Start and enable Apache 25 | action: service name=httpd state=started enabled=yes 26 | 27 | - name: Create OS directory 28 | action: file dest=${packages_path}/minimal state=directory 29 | 30 | - name: Copy CentOS minimal ISO image 31 | # action: copy src=${centos_image} dest=${centos_image} 32 | local_action: command rsync -a --inplace --rsh='ssh -o StrictHostKeyChecking=no' ${centos_image} root@${inventory_hostname}:${centos_image} 33 | 34 | - name: Loopback mount the image 35 | action: mount name=${packages_path}/minimal src=${centos_image} opts=ro,loop fstype=iso9660 state=mounted 36 | 37 | - name: Create Workshop workshop directory 38 | action: file dest=${packages_path}/workshop state=directory 39 | 40 | - name: Copy Workshop RPMS 41 | action: copy src=$item dest=$item 42 | with_fileglob: ${packages_path}/workshop/*.rpm 43 | # with_items: 44 | # - PyYAML-3.10-3.el6.x86_64.rpm 45 | # - ansible-1.0-1.el6.noarch.rpm 46 | # - apr-1.3.9-5.el6_2.i686.rpm 47 | # - apr-util-1.3.9-3.el6_0.1.i686.rpm 48 | # - apr-util-ldap-1.3.9-3.el6_0.1.i686.rpm 49 | # - createrepo-0.9.9-17.el6.noarch.rpm 50 | # - deltarpm-3.5-0.5.20090913git.el6.i686.rpm 51 | # - httpd-2.2.15-26.el6.centos.i686.rpm 52 | # - httpd-tools-2.2.15-26.el6.centos.i686.rpm 53 | # - libxml2-python-2.7.6-8.el6_3.4.i686.rpm 54 | # - libyaml-0.1.3-1.el6.x86_64.rpm 55 | # - mailcap-2.1.31-2.el6.noarch.rpm 56 | # - python-deltarpm-3.5-0.5.20090913git.el6.i686.rpm 57 | # - python-jinja2-2.2.1-1.el6.x86_64.rpm 58 | 59 | - name: Create repository 60 | action: command createrepo ${packages_path}/workshop/ 61 | -------------------------------------------------------------------------------- /plays/library/lvol: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2013, Jeroen Hoekx 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | 21 | DOCUMENTATION = ''' 22 | --- 23 | author: Jeroen Hoekx 24 | module: lvol 25 | short_description: Configure LVM logical volumes 26 | description: 27 | - This module creates, removes or resizes logical volumes. 28 | version_added: "1.1" 29 | options: 30 | vg: 31 | description: 32 | - The volume group this logical volume is part of. 33 | required: true 34 | lv: 35 | description: 36 | - The name of the logical volume. 37 | required: true 38 | size: 39 | description: 40 | - The size of the logical volume in megabytes. 41 | state: 42 | choices: [ "present", "absent" ] 43 | default: present 44 | description: 45 | - Control if the logical volume exists. 46 | required: false 47 | examples: 48 | - description: Create a logical volume of 512m. 49 | code: lvol vg=firefly lv=test size=512 50 | - description: Extend the logical volume to 1024m. 51 | code: lvol vg=firefly lv=test size=1024 52 | - description: Reduce the logical volume to 512m 53 | code: lvol vg=firefly lv=test size=512 54 | - description: Remove the logical volume. 55 | code: lvol vg=firefly lv=test state=absent 56 | notes: 57 | - Filesystems on top of the volume are not resized. 58 | ''' 59 | 60 | def parse_lvs(data): 61 | lvs = [] 62 | for line in data.splitlines(): 63 | parts = line.strip().split(';') 64 | lvs.append({ 65 | 'name': parts[0], 66 | 'size': int(parts[1].split('.')[0]), 67 | 'path': parts[2], 68 | }) 69 | return lvs 70 | 71 | def main(): 72 | module = AnsibleModule( 73 | argument_spec = dict( 74 | vg=dict(required=True), 75 | lv=dict(required=True), 76 | size=dict(), 77 | state=dict(choices=["absent", "present"], default='present'), 78 | ), 79 | ) 80 | 81 | vg = module.params['vg'] 82 | lv = module.params['lv'] 83 | size = module.params['size'] 84 | state = module.params['state'] 85 | 86 | if state=='present' and not size: 87 | module.fail_json(msg="No size given.") 88 | 89 | if size: 90 | size = int(size) 91 | 92 | rc,current_lvs,err = module.run_command("lvs --noheadings -o lv_name,size,lv_path --units m --separator ';' %s"%(vg)) 93 | 94 | if rc != 0: 95 | module.fail_json(msg="Volume group %s does not exist."%vg, rc=rc, err=err) 96 | 97 | changed = False 98 | 99 | lvs = parse_lvs(current_lvs) 100 | 101 | for test_lv in lvs: 102 | if test_lv['name'] == lv: 103 | this_lv = test_lv 104 | break 105 | else: 106 | this_lv = None 107 | 108 | if this_lv is None: 109 | if state == 'present': 110 | ### create LV 111 | rc,_,err = module.run_command("lvcreate -n %s -L %sm %s"%(lv, size, vg)) 112 | if rc == 0: 113 | changed = True 114 | else: 115 | module.fail_json(msg="Creating logical volume '%s' failed"%(lv), rc=rc, err=err) 116 | else: 117 | if state == 'absent': 118 | ### remove LV 119 | rc,_,err = module.run_command("lvremove --force %s"%(this_lv['path'])) 120 | if rc == 0: 121 | module.exit_json(changed=True) 122 | else: 123 | module.fail_json(msg="Failed to remove logical volume %s"%(lv),rc=rc, err=err) 124 | ### resize LV 125 | tool = None 126 | if size > this_lv['size']: 127 | tool = 'lvextend' 128 | elif size < this_lv['size']: 129 | tool = 'lvreduce --force' 130 | 131 | if tool: 132 | rc,_,err = module.run_command("%s -L %sm %s"%(tool, size, this_lv['path'])) 133 | if rc == 0: 134 | changed = True 135 | else: 136 | module.fail_json(msg="Unable to resize %s to %sm."%(lv,size),rc=rc,err=err) 137 | 138 | module.exit_json(changed=changed) 139 | 140 | # this is magic, see lib/ansible/module_common.py 141 | #<> 142 | main() 143 | -------------------------------------------------------------------------------- /plays/library/qemu_img: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2013, Jeroen Hoekx 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | 21 | DOCUMENTATION = ''' 22 | --- 23 | author: Jeroen Hoekx 24 | module: qemu_img 25 | short_description: Create qemu images 26 | description: 27 | - "This module creates images for qemu." 28 | version_added: "1.1" 29 | options: 30 | dest: 31 | description: 32 | - The image file to create or remove 33 | required: true 34 | format: 35 | description: 36 | - The image format - default qcow2 37 | required: false 38 | size: 39 | description: 40 | - The size of the image in megabytes. 41 | required: false 42 | state: 43 | choices: [ "absent", "present" ] 44 | description: 45 | - If the image should be present or absent - default present 46 | required: false 47 | examples: 48 | - description: Create a raw image of 5M. 49 | code: qemu_img dest=/tmp/testimg size=5 format=raw 50 | - description: Enlarge the image to 6M. 51 | code: qemu_img dest=/tmp/testimg size=6 format=raw 52 | - description: Remove the image 53 | code: qemu_img dest=/tmp/testimg state=absent 54 | notes: 55 | - This module does not change the type of the image. 56 | ''' 57 | 58 | import os 59 | 60 | def main(): 61 | 62 | module = AnsibleModule( 63 | argument_spec = dict( 64 | dest=dict(type='str', required=True), 65 | format=dict(type='str', default='qcow2'), 66 | size=dict(type='int'), 67 | state=dict(type='str', choices=['absent', 'present'], default='present'), 68 | ), 69 | ) 70 | 71 | changed = False 72 | qemu_img = module.get_bin_path('qemu-img', True) 73 | 74 | dest = module.params['dest'] 75 | img_format = module.params['format'] 76 | 77 | if module.params['state'] == 'present': 78 | if not module.params['size']: 79 | module.fail_json(msg="Parameter 'size' required") 80 | size = int(module.params['size']) * 1024 * 1024 81 | 82 | if not os.path.exists(dest): 83 | module.run_command('%s create -f %s %s %s'%(qemu_img, img_format, dest, size), check_rc=True) 84 | changed = True 85 | else: 86 | rc, stdout, _ = module.run_command('%s info %s'%(qemu_img, dest), check_rc=True) 87 | current_size = None 88 | for line in stdout.splitlines(): 89 | if 'virtual size' in line: 90 | ### virtual size: 5.0M (5242880 bytes) 91 | current_size = int(line.split('(')[1].split()[0]) 92 | if not current_size: 93 | module.fail_json(msg='Unable to read virtual disk size of %s'%(dest)) 94 | if current_size != size: 95 | module.run_command('%s resize %s %s'%(qemu_img, dest, size), check_rc=True) 96 | changed = True 97 | 98 | if module.params['state'] == 'absent': 99 | if os.path.exists(dest): 100 | os.remove(dest) 101 | changed = True 102 | 103 | module.exit_json(changed=changed) 104 | 105 | # this is magic, see lib/ansible/module_common.py 106 | #<> 107 | main() 108 | -------------------------------------------------------------------------------- /plays/library/virt_boot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2012, Jeroen Hoekx 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | 21 | DOCUMENTATION = ''' 22 | --- 23 | author: Jeroen Hoekx 24 | module: virt_boot 25 | short_description: Define libvirt boot parameters 26 | description: 27 | - "This module configures the boot order or boot media of a libvirt virtual 28 | machine. A guest can be configured to boot from network, hard disk, floppy, 29 | cdrom or a direct kernel boot. Specific media can be attached for cdrom, 30 | floppy and direct kernel boot." 31 | - This module requires the libvirt module. 32 | version_added: "0.8" 33 | options: 34 | domain: 35 | description: 36 | - The name of the libvirt domain. 37 | required: true 38 | boot: 39 | description: 40 | - "Specify the boot order of the virtual machine. This is a comma-separated 41 | list of: I(fd), I(hd), I(cdrom) and I(network)." 42 | required: false 43 | bootmenu: 44 | choices: [ "yes", "no" ] 45 | description: 46 | - Enable or disable the boot menu. 47 | required: false 48 | kernel: 49 | description: 50 | - The path of the kernel to boot. 51 | required: false 52 | initrd: 53 | description: 54 | - The path of the initrd to boot. 55 | required: false 56 | cmdline: 57 | description: 58 | - The command line to boot the kernel with. 59 | required: false 60 | device: 61 | default: hdc 62 | description: 63 | - The libvirt device name of the cdrom/floppy. 64 | required: false 65 | image: 66 | description: 67 | - The image to connect to the cdrom/floppy device. 68 | required: false 69 | start: 70 | choices: [ "yes", "no" ] 71 | default: yes 72 | description: 73 | - Start the guest after configuration. 74 | required: false 75 | examples: 76 | - description: Boot from a cdrom image. 77 | code: virt_boot domain=archrear image=/srv/rear/archrear/rear-archrear.iso boot=cdrom 78 | - description: Boot from the local disk. 79 | code: virt_boot domain=archrear boot=hd 80 | - description: Boot a specific kernel with a special command line. 81 | code: virt_boot domain=archrear kernel=$storage/kernel-archrear initrd=$storage/initramfs-archrear.img cmdline="root=/dev/ram0 vga=normal rw" 82 | - description: Boot from the harddisk and if that fails from the network. 83 | code: virt_boot domain=archrear boot=hd,network 84 | - description: Enable the boot menu. 85 | code: virt_boot domain=archrear bootmenu=yes 86 | requirements: [ "libvirt" ] 87 | notes: 88 | - Run this on the libvirt host. 89 | - I(kernel) and I(boot) are mutually exclusive. 90 | - This module does not change a running system. A shutdown/restart is required. 91 | ''' 92 | 93 | import sys 94 | 95 | try: 96 | import xml.etree.ElementTree as ET 97 | from xml.etree.ElementTree import SubElement 98 | except ImportError: 99 | try: 100 | import elementtree.ElementTree as ET 101 | from elementtree.ElementTree import SubElement 102 | except ImportError: 103 | print "failed=True msg='ElementTree python module unavailable'" 104 | 105 | try: 106 | import libvirt 107 | except ImportError: 108 | print "failed=True msg='libvirt python module unavailable'" 109 | sys.exit(1) 110 | 111 | def get_disk(doc, device): 112 | for disk in doc.findall('.//disk'): 113 | target = disk.find('target') 114 | if target is not None: 115 | if target.get('dev','') == device: 116 | return disk 117 | 118 | def attach_disk(domain, doc, device, image): 119 | disk = get_disk(doc, device) 120 | if disk is not None: 121 | source = disk.find('source') 122 | if source is not None and source.get('file') == image: 123 | return False 124 | 125 | xml = ''' 126 | 127 | 128 | 129 | '''.format(path=image, dev=device) 130 | domain.updateDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CONFIG) 131 | return True 132 | 133 | def detach_disk(domain, doc, device): 134 | disk = get_disk(doc, device) 135 | if disk is not None: 136 | source = disk.find('source') 137 | if source is not None and 'file' in source.attrib: 138 | del source.attrib['file'] 139 | domain.updateDeviceFlags(ET.tostring(disk), libvirt.VIR_DOMAIN_AFFECT_CONFIG) 140 | return True 141 | return False 142 | 143 | def main(): 144 | 145 | module = AnsibleModule( 146 | argument_spec = dict( 147 | domain=dict(required=True, aliases=['guest']), 148 | boot=dict(), 149 | bootmenu=dict(choices=BOOLEANS), 150 | kernel=dict(), 151 | initrd=dict(), 152 | cmdline=dict(), 153 | device=dict(default='hdc'), 154 | image=dict(), 155 | start=dict(choices=BOOLEANS, default='yes'), 156 | ), 157 | required_one_of = [['boot','kernel','image','bootmenu']], 158 | mutually_exclusive = [['boot','kernel']] 159 | ) 160 | 161 | params = module.params 162 | 163 | domain_name = params['domain'] 164 | 165 | bootmenu = module.boolean(params['bootmenu']) 166 | 167 | boot = params['boot'] 168 | kernel = params['kernel'] 169 | initrd = params['initrd'] 170 | cmdline = params['cmdline'] 171 | 172 | device = params['device'] 173 | image = params['image'] 174 | 175 | start = module.boolean(params['start']) 176 | 177 | changed = False 178 | 179 | conn = libvirt.open("qemu:///system") 180 | domain = conn.lookupByName(domain_name) 181 | 182 | doc = ET.fromstring( domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE) ) 183 | 184 | ### Connect image 185 | if image: 186 | changed = changed or attach_disk(domain, doc, device, image) 187 | if not boot and not kernel: 188 | module.exit_json(changed=changed, image=image, device=device) 189 | else: 190 | changed = changed or detach_disk(domain, doc, device) 191 | 192 | if changed: 193 | doc = ET.fromstring( domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE) ) 194 | 195 | ### Boot ordering 196 | os = doc.find('os') 197 | boot_list = os.findall('boot') 198 | kernel_el = os.find('kernel') 199 | initrd_el = os.find('initrd') 200 | cmdline_el = os.find('cmdline') 201 | 202 | ### traditional boot 203 | if boot: 204 | if kernel_el is not None: 205 | changed = True 206 | os.remove(kernel_el) 207 | if initrd_el is not None: 208 | changed = True 209 | os.remove(initrd_el) 210 | if cmdline_el is not None: 211 | changed = True 212 | os.remove(cmdline_el) 213 | 214 | items = boot.split(',') 215 | if boot_list: 216 | needs_change = False 217 | if len(items) == len(boot_list): 218 | for (boot_el, dev) in zip(boot_list, items): 219 | if boot_el.get('dev') != dev: 220 | needs_change = True 221 | else: 222 | needs_change = True 223 | 224 | if needs_change: 225 | changed = True 226 | for boot_el in boot_list: 227 | os.remove(boot_el) 228 | for item in items: 229 | boot_el = SubElement(os, 'boot') 230 | boot_el.set('dev', item) 231 | else: 232 | changed = True 233 | for item in items: 234 | boot_el = SubElement(os, 'boot') 235 | boot_el.set('dev', item) 236 | ### direct kernel boot 237 | elif kernel: 238 | if boot_list: 239 | ### libvirt alwas adds boot=hd using direct kernel boot 240 | if not (len(boot_list)==1 and boot_list[0].get('dev')=='hd'): 241 | changed = True 242 | for boot_el in boot_list: 243 | os.remove(boot_el) 244 | 245 | if kernel_el is not None: 246 | if kernel_el.text != kernel: 247 | changed = True 248 | kernel_el.text = kernel 249 | else: 250 | changed = True 251 | kernel_el = SubElement(os, 'kernel') 252 | kernel_el.text = kernel 253 | 254 | if initrd_el is not None: 255 | if initrd_el.text != initrd: 256 | changed = True 257 | initrd_el.text = initrd 258 | else: 259 | changed = True 260 | initrd_el = SubElement(os, 'initrd') 261 | initrd_el.text = initrd 262 | 263 | if cmdline_el is not None: 264 | if cmdline_el.text != cmdline: 265 | changed = True 266 | cmdline_el.text = cmdline 267 | else: 268 | changed = True 269 | cmdline_el = SubElement(os, 'cmdline') 270 | cmdline_el.text = cmdline 271 | 272 | ### Enable/disable bootmenu 273 | bootmenu_el = os.find('bootmenu') 274 | if bootmenu and bootmenu_el is not None: 275 | bootmenu_enabled = bootmenu_el.get('enable') 276 | if bootmenu_enabled != 'yes': 277 | changed = True 278 | bootmenu_el.set('enable', 'yes') 279 | elif bootmenu: 280 | bootmenu_el = SubElement(os, 'bootmenu') 281 | bootmenu_el.set('enable', 'yes') 282 | changed = True 283 | elif bootmenu_el is not None: 284 | os.remove(bootmenu_el) 285 | changed = True 286 | 287 | ### save back 288 | conn.defineXML( ET.tostring(doc) ) 289 | 290 | if start and not domain.isActive(): 291 | changed = True 292 | domain.create() 293 | 294 | module.exit_json(changed=changed) 295 | 296 | # this is magic, see lib/ansible/module_common.py 297 | #<> 298 | main() 299 | -------------------------------------------------------------------------------- /plays/library/virt_guest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2012, D square NV 5 | # Written by Jeroen Hoekx 6 | # 7 | # This file is part of Ansible 8 | # 9 | # Ansible is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Ansible is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with Ansible. If not, see . 21 | 22 | DOCUMENTATION = ''' 23 | --- 24 | author: Jeroen Hoekx 25 | module: virt_guest 26 | short_description: Define a libvirt guest 27 | description: 28 | - This module creates a libvirt guest based on a given XML definition. 29 | - This module requires the libvirt module. 30 | version_added: "0.9" 31 | options: 32 | connection: 33 | description: 34 | - The connection string for libvirt. 35 | default: qemu:///system 36 | guest: 37 | description: 38 | - The name of the libvirt domain. 39 | required: true 40 | state: 41 | choices: [ "present", "absent" ] 42 | default: "present" 43 | description: 44 | - Create or remove the VM 45 | required: false 46 | src: 47 | description: 48 | - The path of the libvirt XML definition. 49 | required: false 50 | examples: 51 | - description: Create a guest 52 | code: virt_guest guest=${name} src=templates/virt-${name}.xml 53 | - description: Remove a guest 54 | code: virt_guest guest=${name} state=absent 55 | requirements: [ "libvirt" ] 56 | notes: 57 | - Run this on the libvirt host. 58 | - This returns a provisioning_status variable you can register to create groups. 59 | Possible values are: provisioned, unprovisioned, absent 60 | ''' 61 | 62 | import sys 63 | 64 | try: 65 | import libvirt 66 | except ImportError: 67 | print "failed=True msg='libvirt python module unavailable'" 68 | sys.exit(1) 69 | 70 | def main(): 71 | 72 | module = AnsibleModule( 73 | argument_spec = dict( 74 | connection = dict(default='qemu:///system'), 75 | guest = dict(required=True, aliases=['domain']), 76 | state = dict(choices=['present','absent'], default='present'), 77 | src = dict(), 78 | ), 79 | ) 80 | 81 | connection = module.params['connection'] 82 | guest = module.params['guest'] 83 | state = module.params['state'] 84 | src = module.params['src'] 85 | 86 | changed = False 87 | provisioning_status = '' 88 | 89 | try: 90 | conn = libvirt.open(connection) 91 | except Exception, e: 92 | module.fail_json(msg=str(e)) 93 | 94 | try: 95 | domain = conn.lookupByName(guest) 96 | except libvirt.libvirtError: 97 | domain = None 98 | 99 | if state == 'present' and domain: 100 | provisioning_status = 'provisioned' 101 | if state == 'present' and not domain: 102 | if not src: 103 | module.fail_json(msg="Parameter 'src' not defined.") 104 | try: 105 | conn.defineXML( open(src,'r').read() ) 106 | except OSError, e: 107 | module.fail_json(msg=str(e)) 108 | changed = True 109 | provisioning_status = 'unprovisioned' 110 | elif state == 'absent' and domain: 111 | domain.undefine() 112 | changed = True 113 | provisioning_status = 'absent' 114 | 115 | module.exit_json(changed=changed, provisioning_status=provisioning_status) 116 | 117 | # this is magic, see lib/ansible/module_common.py 118 | #<> 119 | main() 120 | -------------------------------------------------------------------------------- /plays/storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Allocate VM storage 4 | hosts: guests 5 | user: root 6 | gather_facts: no 7 | 8 | tasks: 9 | - name: Allocate storage on LVM 10 | action: lvol vg=${storage} lv=lv_${inventory_hostname}_root size=3072 11 | delegate_to: ${hypervisor} 12 | when_set: ${storage} 13 | 14 | - name: Allocate storage in the filesystem 15 | action: qemu_img dest=${qemu_img_path}/${inventory_hostname}.img size=3072 format=qcow2 16 | delegate_to: ${hypervisor} 17 | when_set: ${qemu_img_path} 18 | -------------------------------------------------------------------------------- /plays/vm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create the VM 4 | hosts: guests 5 | user: root 6 | gather_facts: no 7 | 8 | tasks: 9 | - name: VM template folder 10 | action: file dest=/tmp/vm-${inventory_hostname} state=directory 11 | delegate_to: ${hypervisor} 12 | 13 | - name: Prepare VM configuration 14 | action: template src=../templates/vm.xml dest=/tmp/vm-${inventory_hostname}/vm.xml 15 | delegate_to: ${hypervisor} 16 | 17 | - name: Create the VM 18 | action: virt_guest guest=${inventory_hostname} src=/tmp/vm-${inventory_hostname}/vm.xml 19 | delegate_to: ${hypervisor} 20 | register: guest 21 | 22 | - name: Detect new systems 23 | local_action: group_by key=${guest.provisioning_status} 24 | -------------------------------------------------------------------------------- /templates/centos-6.ks: -------------------------------------------------------------------------------- 1 | network --bootproto=static --hostname={{ inventory_hostname }} --ip={{ ip }} --netmask=255.255.255.0 --gateway={{ hypervisor }} --nameserver={{ hypervisor }} 2 | url --url {{ packages_url }}/minimal 3 | repo --name=workshop --baseurl {{ packages_url }}/workshop 4 | 5 | services --enabled=network,postfix,rsyslog --disabled=iptables,iptables-ipv6,sendmail 6 | 7 | install 8 | text 9 | skipx 10 | poweroff 11 | 12 | lang en_US.UTF-8 13 | keyboard us 14 | timezone Etc/UTC 15 | rootpw root 16 | firewall --disabled 17 | authconfig --enableshadow --passalgo=sha512 18 | selinux --disabled 19 | 20 | zerombr 21 | bootloader --location=mbr 22 | clearpart --all --initlabel 23 | part /boot --fstype=ext4 --size=200 --fsoption=noatime 24 | part pv.1 --size 1 --grow 25 | volgroup vg_{{ inventory_hostname }}_root --pesize=4096 pv.1 26 | logvol / --fstype=ext4 --name=lv_root --vgname=vg_{{ inventory_hostname }}_root --size=2048 --fsoptions=noatime 27 | logvol swap --fstype=swap --name=lv_swap --vgname=vg_{{ inventory_hostname }}_root --size=512 28 | 29 | %packages --nobase 30 | -b43-openfwwf 31 | -bridge-utils 32 | -device-mapper-multipath 33 | -iptables-ipv6 34 | -iscsi-initiator-utils 35 | -prelink 36 | -selinux-policy 37 | -selinux-policy-targeted 38 | -system-config-firewall-base 39 | createrepo 40 | crontabs 41 | logrotate 42 | man 43 | man-pages 44 | openssh-clients 45 | openssh-server 46 | rsync 47 | vixie-cron 48 | which 49 | yum 50 | %end 51 | 52 | %post 53 | ### Install the SSH key 54 | mkdir -m0700 /root/.ssh/ 55 | cat </root/.ssh/authorized_keys 56 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCa4iPbNVUYq7Ibkvj/9qI8CmSqRRCXQ/SAg9OA7Md/1UjSMELiMZsGu4A1LHpl4ER8nIet/w78p0amueIYgvX7oVY0+3fkXRqhJzqzoFVG8GzRZgpk9z8qX8aa3Dtq4rIGBH9st5hEcp3xkeap4+sv9xDd6X8Bd5gvYaCwvbU/vlgE6iYNpp45QNEaUOx50jHD3zPU6jShuJm/SnKmxW2HjXMY9DesYil5Dh2ixrYHoFjT1G/S1y+5plpTmylymd73oeu2cl04ImfT99Iufn7GAgjisSSDFC4o04jzm8bAzMKPf8/0iN1UrHmuR9rvmRqo3yWb7LTYdygSmqDOe5FB ansible@workshop 57 | EOF 58 | chmod 0600 /root/.ssh/authorized_keys 59 | restorecon -R /root/.ssh/ 60 | -------------------------------------------------------------------------------- /templates/etc/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 2 | ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 3 | 4 | {% for machine in groups['guests'] %} 5 | {{ hostvars[machine].ip }} {{ machine }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /templates/etc/httpd/conf.d/packages.conf: -------------------------------------------------------------------------------- 1 | Alias /packages {{ packages_path }} 2 | 3 | 4 | Options +Indexes 5 | Order allow,deny 6 | Allow from all 7 | 8 | -------------------------------------------------------------------------------- /templates/etc/httpd/conf.d/packages.conf.v2: -------------------------------------------------------------------------------- 1 | ### Mount the rpm package directory 2 | Alias /packages {{ packages_path }} 3 | 4 | 5 | Options +Indexes 6 | Order allow,deny 7 | Allow from all 8 | 9 | -------------------------------------------------------------------------------- /templates/etc/ssh/ssh_config: -------------------------------------------------------------------------------- 1 | # $OpenBSD: ssh_config,v 1.25 2009/02/17 01:28:32 djm Exp $ 2 | 3 | # This is the ssh client system-wide configuration file. See 4 | # ssh_config(5) for more information. This file provides defaults for 5 | # users, and the values can be changed in per-user configuration files 6 | # or on the command line. 7 | 8 | # Configuration data is parsed as follows: 9 | # 1. command line options 10 | # 2. user-specific file 11 | # 3. system-wide file 12 | # Any configuration value is only changed the first time it is set. 13 | # Thus, host-specific definitions should be at the beginning of the 14 | # configuration file, and defaults at the end. 15 | 16 | # Site-wide defaults for some commonly used options. For a comprehensive 17 | # list of available options, their meanings and defaults, please see the 18 | # ssh_config(5) man page. 19 | 20 | # Host * 21 | # ForwardAgent no 22 | # ForwardX11 no 23 | # RhostsRSAAuthentication no 24 | # RSAAuthentication yes 25 | # PasswordAuthentication yes 26 | # HostbasedAuthentication no 27 | # GSSAPIAuthentication no 28 | # GSSAPIDelegateCredentials no 29 | # GSSAPIKeyExchange no 30 | # GSSAPITrustDNS no 31 | # BatchMode no 32 | # CheckHostIP yes 33 | # AddressFamily any 34 | # ConnectTimeout 0 35 | # StrictHostKeyChecking ask 36 | # IdentityFile ~/.ssh/identity 37 | # IdentityFile ~/.ssh/id_rsa 38 | # IdentityFile ~/.ssh/id_dsa 39 | # Port 22 40 | # Protocol 2,1 41 | # Cipher 3des 42 | # Ciphers aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc 43 | # MACs hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160 44 | # EscapeChar ~ 45 | # Tunnel no 46 | # TunnelDevice any:any 47 | # PermitLocalCommand no 48 | # VisualHostKey no 49 | Host * 50 | GSSAPIAuthentication yes 51 | StrictHostKeyChecking no 52 | # If this option is set to yes then remote X11 clients will have full access 53 | # to the original X11 display. As virtually no X11 client supports the untrusted 54 | # mode correctly we set this to yes. 55 | ForwardX11Trusted yes 56 | # Send locale-related environment variables 57 | SendEnv LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES 58 | SendEnv LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT 59 | SendEnv LC_IDENTIFICATION LC_ALL LANGUAGE 60 | SendEnv XMODIFIERS 61 | -------------------------------------------------------------------------------- /templates/etc/yum.repos.d/workshop.repo: -------------------------------------------------------------------------------- 1 | [minimal] 2 | name=CentOS minimal 3 | baseurl={{ packages_url }}/minimal 4 | enabled=1 5 | gpgcheck=1 6 | gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 7 | 8 | [base] 9 | name=CentOS base 10 | mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os 11 | enabled=0 12 | gpgcheck=1 13 | gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 14 | 15 | [updates] 16 | name=CentOS updates 17 | mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates 18 | enabled=0 19 | gpgcheck=1 20 | gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 21 | 22 | [workshop] 23 | name=Ansible workshop 24 | baseurl={{ packages_url }}/workshop 25 | enabled=1 26 | gpgcheck=0 27 | -------------------------------------------------------------------------------- /templates/root/ssh/id_rsa.workshop.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCa4iPbNVUYq7Ibkvj/9qI8CmSqRRCXQ/SAg9OA7Md/1UjSMELiMZsGu4A1LHpl4ER8nIet/w78p0amueIYgvX7oVY0+3fkXRqhJzqzoFVG8GzRZgpk9z8qX8aa3Dtq4rIGBH9st5hEcp3xkeap4+sv9xDd6X8Bd5gvYaCwvbU/vlgE6iYNpp45QNEaUOx50jHD3zPU6jShuJm/SnKmxW2HjXMY9DesYil5Dh2ixrYHoFjT1G/S1y+5plpTmylymd73oeu2cl04ImfT99Iufn7GAgjisSSDFC4o04jzm8bAzMKPf8/0iN1UrHmuR9rvmRqo3yWb7LTYdygSmqDOe5FB ansible@workshop 2 | -------------------------------------------------------------------------------- /templates/vm.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ inventory_hostname }} 3 | 524288 4 | 1 5 | 6 | 7 | hvm 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | destroy 21 | restart 22 | restart 23 | 24 | 25 | 26 | {% if qemu_img_path %} 27 | 28 | 29 | {% else %} 30 | 31 | 32 | {% endif %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | --------------------------------------------------------------------------------