├── .gitattributes ├── all.yaml ├── .gitignore ├── roles └── hostconf-esxi │ ├── templates │ ├── resolv.conf.j2 │ ├── ntp.conf.j2 │ └── gen_keys.sh.j2 │ ├── vars │ └── main.yml │ ├── tasks │ ├── dns.yml │ ├── main.yml │ ├── hostname.yml │ ├── license.yml │ ├── certs.yml │ ├── software.yml │ ├── ntp.yml │ ├── logging.yml │ ├── autostart.yml │ ├── network.yml │ ├── users.yml │ └── storage.yml │ ├── handlers │ └── main.yml │ ├── files │ ├── id_rsa.alex@alex-mp.pub │ └── profile.local │ ├── defaults │ └── main.yml │ ├── README.md │ └── meta │ └── main.yml ├── update_esxi_defaults.yaml ├── get_vault_pass.esxi.sh ├── vm_deploy ├── clone_vars.example.yaml ├── ansible-deploy.esxi.cfg ├── replace.py-2.2.orig.py ├── replace.py-2.2_fixed_for_python3.py ├── upload_clone.yaml └── clone_local.yaml ├── group_vars ├── all-m0.yaml └── all.yaml ├── filter_plugins ├── torec.py ├── todict.py └── split.py ├── inventory.esxi ├── ansible.esxi.cfg ├── host_vars └── cage.yaml ├── library ├── esxi_vib.py ├── esxi_vm_info.py └── esxi_autostart.py ├── update_esxi.yaml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | vault.* diff=vault 2 | *.vault.* diff=vault 3 | *.vault diff=vault 4 | -------------------------------------------------------------------------------- /all.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all.esxi 3 | roles: 4 | - hostconf-esxi 5 | tags: 6 | - hostconf 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | ansible.log 3 | *.out 4 | *.pyc 5 | *.sublime-project 6 | *.sublime-workspace 7 | /tmp/ 8 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/templates/resolv.conf.j2: -------------------------------------------------------------------------------- 1 | domain {{ dns_domain }} 2 | search {{ dns_domain }} 3 | {% for server in name_servers %} 4 | nameserver {{ server }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # to shorten command lines 3 | asm_cmd: vim-cmd hostsvc/autostartmanager 4 | 5 | esxi_fqdn: "{{ inventory_hostname }}.{{ dns_domain }}" 6 | 7 | vmfs_guid: "AA31E02A400F11DB9590000C2911D1B8" 8 | -------------------------------------------------------------------------------- /update_esxi_defaults.yaml: -------------------------------------------------------------------------------- 1 | default_http_src: "http://www-distr.m1.maxidom.ru/suse_distr/iso/" 2 | default_temp_dir: "{{ ((local_datastores|d({'def': ansible_hostname + '-sys'})) | dictsort | first)[1] }}" 3 | 4 | force_reboot: false 5 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # required vars: dns_servers 3 | - name: (dns) create resolver config file 4 | template: 5 | src: "resolv.conf.j2" 6 | dest: "/etc/resolv.conf" 7 | owner: root 8 | group: root 9 | mode: 0644 10 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/templates/ntp.conf.j2: -------------------------------------------------------------------------------- 1 | ### {{ ansible_managed }} 2 | {% for server in ntp_servers %} 3 | server {{ server }} 4 | {% endfor %} 5 | driftfile /etc/ntp.drift 6 | {% for server in ntp_servers %} 7 | restrict {{ server }} noquery nomodify notrap 8 | {% endfor %} 9 | restrict default ignore 10 | restrict 127.0.0.1 nopeer 11 | -------------------------------------------------------------------------------- /get_vault_pass.esxi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # to store (-l is the label, mb will be different later): 3 | # security add-generic-password -a all -D "Ansible Vault" -s "ansible_vault" -l "pass-kind" -w 'pass-here!' 4 | # ansible allows to prompt for data (to stdout) and have different passwords here 5 | security find-generic-password -s "ansible_vault" -l "esxi" -w 6 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - include: hostname.yml 2 | - include: license.yml 3 | when: esxi_serial is defined 4 | - include: dns.yml 5 | - include: ntp.yml 6 | - include: users.yml 7 | - include: network.yml 8 | - include: storage.yml 9 | - include: autostart.yml 10 | - include: logging.yml 11 | - include: certs.yml 12 | - include: software.yml -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/hostname.yml: -------------------------------------------------------------------------------- 1 | - name: (hostname) get host name 2 | shell: "esxcli system hostname get | awk '/Fully Qualified / {print $5}'" 3 | register: hostname_res 4 | failed_when: false 5 | changed_when: false 6 | check_mode: false 7 | 8 | - name: (hostname) assign host name 9 | command: "esxcli system hostname set --fqdn {{ esxi_fqdn }}" 10 | when: hostname_res.stdout != esxi_fqdn 11 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: reload syslog config 2 | command: "esxcli system syslog reload" 3 | 4 | - name: restart vpxa 5 | command: "/etc/init.d/vpxa restart" 6 | 7 | - name: restart rhttpproxy 8 | command: "/etc/init.d/rhttpproxy restart" 9 | 10 | - name: restart hostd 11 | command: "/etc/init.d/hostd restart" 12 | 13 | - name: restart ntpd 14 | command: "/etc/init.d/ntpd restart" 15 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/files/id_rsa.alex@alex-mp.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAGbBRP+zrGO+/6JZxeP2hoKrQmFjlQbRykHMhXA5nVXQvTvPqQiv5tsgYH+Tny1vatekqz1AfM5o4YxXxt6uxrwIXmgiXiM7e6pBkl5WPO9nIpINYko504jhHFFWAd1XxHSIxuOxS3oRKmWr/hGutF1yBEz8nc1Cx+UHELq9CtHAvKaYaZkOFJWkhg7cL7c/uP/qTr2K3zi8/LhmOntZqVqeRdQw2hQi6EqXmLKitoJDy3cHowBA8cd2n39aQ8BSBFtJk+8tFhpotQMkJ8pJ1Co2TDDah9vuIjJvSCIWqv6NJHNCwR04XyAH4+JcP6Rhf82fg4KXHfTFgxGNG2rJ3 alex@alex-mbp15.local -------------------------------------------------------------------------------- /vm_deploy/clone_vars.example.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ## variables for cloning: example 3 | 4 | ## dest: optional 5 | # default: like src 6 | dst_vm_name: phoenix11-t 7 | # default: 1st datastore 8 | dst_vm_path: nest-test-sys 9 | # default: look up with dns (name + dst host domain) 10 | dst_vm_ip: 10.1.10.123 11 | dst_vm_gw: 10.1.10.254 12 | 13 | ## source: usually m0 template 14 | #src_vm_server: cage7 15 | #src_vm_name: phoenix11 16 | #src_vm_path: infra.data 17 | -------------------------------------------------------------------------------- /group_vars/all-m0.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars for m0 3 | dns_domain: "m0.maxidom.ru" 4 | name_servers: 5 | - 10.0.128.1 6 | - 10.0.128.2 7 | 8 | ntp_servers: 9 | - 10.1.131.1 10 | - 10.1.131.2 11 | 12 | vms_to_autostart: 13 | eagle-m0: 14 | order: 1 15 | hawk-m0: 16 | order: 2 17 | falcon-m0: 18 | 19 | # autostart only listed VMs 20 | autostart_only_listed: true 21 | 22 | # really ok with defaults: "log." + dns_domain 23 | # syslog_host: log.m0.maxidom.ru 24 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/files/profile.local: -------------------------------------------------------------------------------- 1 | PS1='\u@\h:\W\$ ' 2 | if [ $TERM = xterm -o $TERM = xterm-color ]; then 3 | # original 4 | PS1='\[\033[36m\]\u\[\033[1;36m\]@\h\[\033[0;32m\]:\W\[\033[0m\]\$ \[\033]2;\u@\h:\w\007\]' 5 | fi 6 | if [ $TERM = xterm-256color ]; then 7 | # softer colors 8 | PS1='\[\033[36m\]\u\[\033[1;36m\]@\[\033[0;36m\]\h\[\033[0;32m\]:\W\[\033[0m\]\$ \[\033]2;\u@\h:\w\007\]' 9 | # set term to xterm for esxtop to work 10 | export TERM=xterm 11 | fi 12 | -------------------------------------------------------------------------------- /filter_plugins/torec.py: -------------------------------------------------------------------------------- 1 | from ansible import errors 2 | import re 3 | 4 | def to_rec(arr, fields): 5 | """ convert array to dictionary, naming records after another array """ 6 | if len(arr) != len(fields): 7 | raise errors.AnsibleFilterError('to_rec: expected %d fields, got %d' % (len(fields), len(arr))) 8 | return dict(zip(fields, arr)) 9 | 10 | class FilterModule(object): 11 | ''' A filter to convert array into record ''' 12 | def filters(self): 13 | return { 14 | 'record' : to_rec 15 | } 16 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/templates/gen_keys.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | {% for user, userinfo in (esxi_local_users | dictsort) if userinfo['pubkeys'] is defined %} 4 | [ -d /etc/ssh/keys-{{ user }} ] || mkdir /etc/ssh/keys-{{ user }} 5 | cat > /etc/ssh/keys-{{ user }}/authorized_keys << EOT 6 | {% for key_info in userinfo['pubkeys'] %} 7 | {% if key_info['hosts'] is defined %}from="{{ key_info['hosts'] }},{{ permit_ssh_from }}" {% endif %} 8 | {{ lookup('file', 'id_rsa.' + user + '@' + key_info['name'] + '.pub') }} 9 | {% endfor %} 10 | EOT 11 | 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/license.yml: -------------------------------------------------------------------------------- 1 | - name: (license) get host license 2 | shell: "vim-cmd vimsvc/license --show | awk '/serial: / {print $2}'" 3 | register: lic_info 4 | failed_when: false 5 | changed_when: false 6 | check_mode: false 7 | 8 | - name: (license) print host license 9 | debug: 10 | msg: "license: {{ lic_info.stdout }}" 11 | when: lic_info.stdout != '' 12 | 13 | - name: (license) assign license 14 | command: "vim-cmd vimsvc/license --set {{ esxi_serial }}" 15 | when: lic_info.stdout == '' or lic_info.stdout == '00000-00000-00000-00000-00000' 16 | -------------------------------------------------------------------------------- /filter_plugins/todict.py: -------------------------------------------------------------------------------- 1 | import re 2 | from ansible import errors 3 | 4 | 5 | def to_dict(rec_arr, key): 6 | """ convert array of records to dictionary key -> record """ 7 | return {item[key]: item for item in rec_arr} 8 | 9 | 10 | def to_dict_flat(rec_arr): 11 | """ convert array of records to dictionary rec[0] -> rec[1] """ 12 | return {item[0]: item[1] for item in rec_arr} 13 | 14 | 15 | class FilterModule(object): 16 | ''' A filter to convert list of records into dict of records ''' 17 | def filters(self): 18 | return { 19 | 'to_dict': to_dict, 20 | 'to_dict_flat': to_dict_flat 21 | } 22 | -------------------------------------------------------------------------------- /inventory.esxi: -------------------------------------------------------------------------------- 1 | # specifying inventory 2 | # - export ANSIBLE_CONFIG=~/esxi-mgmt/ansible.esxi.cfg 3 | # and ansible-playbook all.yaml -l nest1-u1 --diff 4 | # - export ANSIBLE_HOSTS=inventory.esxi 5 | # - use "-i inventory.esxi" 6 | # like ansible-playbook all.yaml -i inventory.esxi --tags hostconf -l nest1-m8 --diff 7 | 8 | # works ok for 6.0+, 5.5 python version is too old 9 | 10 | # initial hostconf: run with "-u root -k" 11 | # like: ansible-playbook all.yaml -l nest1-u1 --diff -u root -k --tags hostconf 12 | 13 | # all hosts: include sites here 14 | [all.esxi:children] 15 | all-m0 16 | 17 | # one site with specific vars in group_vars 18 | [all-m0] 19 | cage ansible_host=cage.m0.maxidom.ru 20 | -------------------------------------------------------------------------------- /ansible.esxi.cfg: -------------------------------------------------------------------------------- 1 | # export ANSIBLE_CONFIG=~/ansible-esxi/ansible.esxi.cfg 2 | # ansible-playbook all.yaml -l nest1-m6 --check 3 | # ansible-playbook all.yaml -l nest1-m6 --diff 4 | # (or for new host): ansible-playbook all.yaml -l nest1-m8 --ask-pass --diff 5 | 6 | [ssh_connection] 7 | pipelining = true 8 | 9 | [defaults] 10 | #vault_password_file=/Users/alex/.vaultpass.test 11 | #ask_vault_pass = true 12 | vault_password_file = /Users/alex/ansible-esxi/get_vault_pass.esxi.sh 13 | retry_files_enabled = false 14 | remote_user = alex 15 | log_path = /Users/alex/ansible-esxi/ansible.log 16 | inventory = /Users/alex/ansible-esxi/inventory.esxi 17 | library = /Users/alex/ansible-esxi/library 18 | ansible_managed = ansible managed: last modified by {uid}@{host} 19 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/certs.yml: -------------------------------------------------------------------------------- 1 | # put pub key .pem into .rui.crt 2 | # put priv key .priv.pem into .rui.key.vault 3 | # encrypt it with "ansible-vault encrypt .rui.key.vault" 4 | 5 | # to regenerate self-signed: "/sbin/generate-certificates" 6 | 7 | - name: (certs) check if server certificate is present 8 | stat: 9 | path: "{{ inventory_dir }}/files/{{ inventory_hostname }}.rui.crt" 10 | register: cert_check_res 11 | 12 | - name: (certs) copy server certificats 13 | copy: 14 | src: "{{ inventory_dir }}/files/{{ inventory_hostname }}.{{ item }}" 15 | dest: "/etc/vmware/ssl/{{ item | replace('.vault', '') }}" 16 | owner: root 17 | group: root 18 | mode: 0644 19 | with_items: 20 | - "rui.crt" 21 | - "rui.key.vault" 22 | when: 23 | - cert_check_res.stat.exists 24 | notify: 25 | - restart rhttpproxy -------------------------------------------------------------------------------- /filter_plugins/split.py: -------------------------------------------------------------------------------- 1 | from ansible import errors 2 | import re 3 | 4 | # https://github.com/timraasveld/ansible-string-split-filter/blob/master/split.py 5 | 6 | def split_string(string, separator=None, maxsplit=-1): 7 | try: 8 | return string.split(separator, maxsplit) 9 | except Exception, e: 10 | raise errors.AnsibleFilterError('split plugin error: %s, provided string: "%s"' % (str(e),str(string)) ) 11 | 12 | def split_regex(string, separator_pattern='\s+'): 13 | try: 14 | return re.split(separator_pattern, string) 15 | except Exception, e: 16 | raise errors.AnsibleFilterError('split plugin error: %s, provided string: "%s"' % (str(e),str(string)) ) 17 | 18 | class FilterModule(object): 19 | ''' A filter to split a string into a list. ''' 20 | def filters(self): 21 | return { 22 | 'split' : split_string, 23 | 'split_regex' : split_regex, 24 | } 25 | -------------------------------------------------------------------------------- /group_vars/all.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | #esxi_serial: "XXXXX-XXXXX-XXXX-XXXXX-XXXXX" 3 | 4 | # host names are in fact ignored by esxi; better use ip here 5 | esxi_local_users: 6 | "alex": 7 | desc: "Alexey Vekshin" 8 | pubkeys: 9 | - name: "alex-mp" 10 | hosts: "10.1.11.6,alex-mp.m1.maxidom.ru" 11 | 12 | # same default port groups for all for now 13 | # for multiple vSwitches: either add 2nd record field or keep separate var for 14 | # 2nd switch 15 | # actually, mX requires only adm-srv, srv-smb, vMotion (optional) and all-tagged: 16 | # for now hosts with 3+ vlans have 2 cards (adm-srv + all-tagged) and vlans in 17 | # sub-interfaces so per-vlan portgroups are for pure aesthetics 18 | esxi_portgroups: 19 | # bare minimum 20 | "vMotion": 21 | tag: 241 22 | "all-tagged": 23 | tag: 4095 24 | "adm-srv": 25 | tag: 210 26 | 27 | # install host client 28 | vib_list: 29 | - name: esx-ui 30 | url: "http://www-distr.m1.maxidom.ru/suse_distr/iso/esxui-signed-6360286.vib" 31 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # very dummy defaults 3 | #ntp_servers: 4 | # - 127.0.0.1 5 | 6 | #dns_servers: 7 | # - 127.0.0.1 8 | 9 | ssh_timeout: 3600 10 | 11 | # mostly ok: dns_domain is overriden per-group 12 | syslog_host: "log.{{ dns_domain }}" 13 | 14 | # filesystem names: 1st is "hostname-sys" by default 15 | local_datastores: {} 16 | # "vmhba0:C0:T0:L1": "{{ inventory_hostname }}-sys" 17 | 18 | # rename (empty) datastores 19 | rename_datastores: true 20 | 21 | # create datastores on vacant luns 22 | create_datastores: true 23 | 24 | # add those hosts to permitted host lists for forwarded keys 25 | permit_ssh_from: 192.168.0.* 26 | 27 | # disable autostart for VMs not in autostart list 28 | autostart_only_listed: false 29 | 30 | # default vSwitch to operate on 31 | vswitch_def: vSwitch0 32 | 33 | # vmotion iface: vmotion IP is ... 34 | create_vmotion_iface: false 35 | vmotion_iface_name: vmk1 36 | vmotion_portgroup_name: vMotion 37 | vmotion_subnet_number: 241 38 | -------------------------------------------------------------------------------- /vm_deploy/ansible-deploy.esxi.cfg: -------------------------------------------------------------------------------- 1 | # see http://docs.ansible.com/ansible/intro_configuration.html 2 | # export ANSIBLE_CONFIG=/Users/alex/works/sysadm/ansible-study/esxi-mgmt/ansible.esxi.cfg 3 | # ansible-playbook all.yaml -l nest1-m6 --check 4 | # ansible-playbook all.yaml -l nest1-m6 --diff 5 | # (or for new host): ansible-playbook all.yaml -l nest1-m8 --ask-pass --diff 6 | 7 | [ssh_connection] 8 | pipelining = true 9 | # alt: use vars: ansible_ssh_extra_args: '-A' 10 | ssh_args=-o ForwardAgent=yes 11 | 12 | [defaults] 13 | #vault_password_file=/Users/alex/.vaultpass.test 14 | #ask_vault_pass = true 15 | vault_password_file = /Users/alex/ansible-esxi/get_vault_pass.esxi.sh 16 | retry_files_enabled = false 17 | remote_user = alex 18 | log_path = /Users/alex/ansible-esxi/ansible.log 19 | inventory = /Users/alex/ansible-esxi/inventory.esxi 20 | library = /Users/alex/ansible-esxi/library 21 | ansible_managed = ansible managed: last modified by {uid}@{host} 22 | # store large files there: vars are ok! 23 | remote_tmp = $(df | awk 'NR==2 {print $6}')/tmp 24 | -------------------------------------------------------------------------------- /host_vars/cage.yaml: -------------------------------------------------------------------------------- 1 | # local datastores to create 2 | local_datastores: 3 | "vmhba0:C0:T0:L1": "cage-sys" 4 | "vmhba0:C0:T0:L2": "cage-apps" 5 | create_datastores: true 6 | 7 | # minimal for cage 8 | esxi_portgroups: 9 | all-tagged: { tag: 4095 } 10 | adm-srv: { tag: 210 } 11 | # do not really need it now 12 | vMotion: { tag: 241 } 13 | # services here 14 | srv-smb: { tag: 128 } 15 | srv-netinf: { tag: 131 } 16 | pvt-netinf: { tag: 199 } 17 | 18 | # users: as usual + andrey 19 | esxi_local_users: 20 | "alex": 21 | desc: "Alexey Vekshin" 22 | pubkeys: 23 | - name: "alex-mp" 24 | hosts: "10.1.11.6,alex-mp" 25 | - name: "alex-mbp" 26 | hosts: "alex-mbp15*,alex-mp.m1.maxidom.ru" 27 | 28 | # newer host client 29 | vib_list: 30 | - name: esx-ui 31 | url: "http://www-distr.m1.maxidom.ru/suse_distr/iso/esxui-signed-7119706.vib" 32 | 33 | # default clone source 34 | src_vm_name: phoenix11-test 35 | #src_vm_server: cage7 36 | #src_vm_vol: infra.data 37 | #dst_vm_vol: nest1-m1-storage1 38 | #dst_vm_net: adm-srv 39 | 40 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/README.md: -------------------------------------------------------------------------------- 1 | Role Name 2 | ========= 3 | 4 | A brief description of the role goes here. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. 10 | 11 | Role Variables 12 | -------------- 13 | 14 | A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. 15 | 16 | Dependencies 17 | ------------ 18 | 19 | A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. 20 | 21 | Example Playbook 22 | ---------------- 23 | 24 | Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: 25 | 26 | - hosts: servers 27 | roles: 28 | - { role: username.rolename, x: 42 } 29 | 30 | License 31 | ------- 32 | 33 | BSD 34 | 35 | Author Information 36 | ------------------ 37 | 38 | An optional section for the role authors to include contact information, or a website (HTML is not allowed). 39 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/software.yml: -------------------------------------------------------------------------------- 1 | # install or update some VIB 2 | 3 | - name: (logging) check http client ruleset state 4 | command: "esxcli network firewall ruleset list --ruleset-id=httpClient" 5 | register: http_ruleset_state 6 | changed_when: false 7 | check_mode: false 8 | 9 | - name: (logging) enable syslog client through firewall 10 | command: "esxcli network firewall ruleset set --ruleset-id=httpClient --enabled=true" 11 | when: http_ruleset_state.stdout.find("false") != -1 12 | 13 | - name: (software) make sure required VIBs are installed 14 | esxi_vib: 15 | name: "{{ item.name }}" 16 | url: "{{ item.url }}" 17 | # present for install and not update (faster check) 18 | state: latest 19 | with_items: "{{ vib_list|d([]) }}" 20 | 21 | - block: 22 | 23 | - name: (software) check slpd ruleset state 24 | command: "esxcli network firewall ruleset list --ruleset-id=CIMSLP" 25 | register: slpd_ruleset_state 26 | changed_when: false 27 | check_mode: false 28 | 29 | - name: (software) disable access to slpd through firewall 30 | command: "esxcli network firewall ruleset set --ruleset-id=CIMSLP --enabled=false" 31 | when: slpd_ruleset_state.stdout.find("false") == -1 32 | 33 | # better to check it like "chkconfig --list slpd" 34 | - name: (software) disable slpd startup 35 | command: "chkconfig slpd off" 36 | when: slpd_ruleset_state.stdout.find("false") == -1 37 | 38 | - name: (software) stop slpd 39 | command: "/etc/init.d/slpd stop" 40 | when: slpd_ruleset_state.stdout.find("false") == -1 41 | 42 | when: disable_slpd|d(false) 43 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # required vars: ntp_servers 3 | 4 | # could use random element from list of servers, like {{ ntp_servers | random }} 5 | - name: (ntp) update ntp config file 6 | template: 7 | src: "ntp.conf.j2" 8 | dest: "/etc/ntp.conf" 9 | owner: root 10 | group: root 11 | mode: 0644 12 | notify: restart ntpd 13 | 14 | - name: (ntp) check ntp client ruleset state 15 | command: "esxcli network firewall ruleset list --ruleset-id=ntpClient" 16 | register: ntp_ruleset_state 17 | changed_when: false 18 | check_mode: false 19 | 20 | - name: (ntp) enable ntp client through firewall 21 | command: "esxcli network firewall ruleset set --ruleset-id=ntpClient --enabled=true" 22 | when: ntp_ruleset_state.stdout.find("false") != -1 23 | notify: restart ntpd 24 | 25 | # "service" is not implemented for esxi; "ntpd is running"/"ntpd is not running" 26 | - name: (ntp) check ntp service state 27 | command: "/etc/init.d/ntpd status" 28 | register: ntp_service_state 29 | failed_when: ntp_service_state.rc > 3 30 | check_mode: false 31 | changed_when: false 32 | 33 | # notify handler (actually start service) if not yet running 34 | - name: (ntp) set time if ntp is not running 35 | command: "ntpd -g -q" 36 | when: ntp_service_state.rc != 0 37 | notify: restart ntpd 38 | 39 | - name: (ntp) check ntp autostart 40 | command: "chkconfig ntpd" 41 | register: ntpd_autostart_state 42 | failed_when: false 43 | check_mode: false 44 | changed_when: false 45 | 46 | - name: (ntp) enable ntpd autostart 47 | command: "chkconfig ntpd on" 48 | when: ntpd_autostart_state.rc != 0 49 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: your name 3 | description: your description 4 | company: your company (optional) 5 | 6 | # If the issue tracker for your role is not on github, uncomment the 7 | # next line and provide a value 8 | # issue_tracker_url: http://example.com/issue/tracker 9 | 10 | # Some suggested licenses: 11 | # - BSD (default) 12 | # - MIT 13 | # - GPLv2 14 | # - GPLv3 15 | # - Apache 16 | # - CC-BY 17 | license: license (GPLv2, CC-BY, etc) 18 | 19 | min_ansible_version: 1.2 20 | 21 | # Optionally specify the branch Galaxy will use when accessing the GitHub 22 | # repo for this role. During role install, if no tags are available, 23 | # Galaxy will use this branch. During import Galaxy will access files on 24 | # this branch. If travis integration is cofigured, only notification for this 25 | # branch will be accepted. Otherwise, in all cases, the repo's default branch 26 | # (usually master) will be used. 27 | #github_branch: 28 | 29 | # 30 | # Below are all platforms currently available. Just uncomment 31 | # the ones that apply to your role. If you don't see your 32 | # platform on this list, let us know and we'll get it added! 33 | # 34 | #platforms: 35 | 36 | galaxy_tags: [] 37 | # List tags for your role here, one per line. A tag is 38 | # a keyword that describes and categorizes the role. 39 | # Users find roles by searching for tags. Be sure to 40 | # remove the '[]' above if you add tags to this list. 41 | # 42 | # NOTE: A tag is limited to a single word comprised of 43 | # alphanumeric characters. Maximum 20 tags per role. 44 | 45 | dependencies: [] 46 | # List your role dependencies here, one per line. 47 | # Be sure to remove the '[]' above if you add dependencies 48 | # to this list. -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/logging.yml: -------------------------------------------------------------------------------- 1 | - name: (logging) get loghost settings 2 | shell: "esxcli system syslog config get | awk '/^ Remote Host:/ {print $3}'" 3 | register: loghost_res 4 | changed_when: false 5 | check_mode: false 6 | 7 | - name: (logging) set loghost name 8 | command: "esxcli system syslog config set --loghost udp://{{ syslog_host }}" 9 | when: loghost_res.stdout != ("udp://" + syslog_host) 10 | notify: reload syslog config 11 | 12 | - name: (logging) check syslog client ruleset state 13 | command: "esxcli network firewall ruleset list --ruleset-id=syslog" 14 | register: syslog_ruleset_state 15 | changed_when: false 16 | check_mode: false 17 | 18 | - name: (logging) enable syslog client through firewall 19 | command: "esxcli network firewall ruleset set --ruleset-id=syslog --enabled=true" 20 | when: syslog_ruleset_state.stdout.find("false") != -1 21 | 22 | # better use "xml" for that: will not get added if completely missed 23 | - name: (logging) set vpxa logging level to info 24 | lineinfile: 25 | dest: "/etc/vmware/vpxa/vpxa.cfg" 26 | insertafter: "^\\s+<(!-- default log level --|log)>" 27 | regexp: "^\\s+[a-z]+$" 28 | line: " info" 29 | notify: restart vpxa 30 | 31 | - name: (logging) set rhttpproxy logging level to info 32 | lineinfile: 33 | dest: "/etc/vmware/rhttpproxy/config.xml" 34 | insertafter: "^\\s+<(!-- default log level --|log)>" 35 | regexp: "^\\s+[a-z]+$" 36 | line: " info" 37 | notify: restart rhttpproxy 38 | 39 | - name: (logging) set hostd logging level to info 40 | lineinfile: 41 | dest: "/etc/vmware/hostd/config.xml" 42 | insertafter: "^\\s+<(!-- default log level --|log)>" 43 | regexp: "^\\s+[a-z]+$" 44 | line: " info" 45 | notify: restart hostd 46 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/autostart.yml: -------------------------------------------------------------------------------- 1 | # mostly dealing with autostart now 2 | 3 | - name: (autostart) get autostart options list 4 | shell: "{{ asm_cmd }}/get_defaults | awk 'NR > 1 && !/^}/ {print $1, $3}' | sed -e 's/,$//'" 5 | register: autostart_opts_list_res 6 | changed_when: false 7 | check_mode: false 8 | 9 | - name: (autostart) convert autostart options to structure 10 | set_fact: 11 | # convert to flat dict "name" -> "value" (key is 1st record element, value is 2nd) 12 | autostart_opts: "{{ autostart_opts_list_res.stdout_lines 13 | | map('split', None, 1) 14 | | to_dict_flat }}" 15 | 16 | #- name: print them 17 | # debug: 18 | # var: autostart_opts 19 | 20 | - name: (autostart) enable autostart 21 | command: "{{ asm_cmd }}/enable_autostart true" 22 | when: autostart_opts.enabled != "true" 23 | 24 | - name: (autostart) set autostart defaults 25 | command: "{{ asm_cmd }}/update_defaults 120 120 'guestShutdown' true" 26 | when: autostart_opts.stopAction != '"guestShutdown"' 27 | 28 | - name: (autostart) get list of VMs 29 | esxi_vm_info: 30 | get_start_state: true 31 | register: vm_info 32 | 33 | # autostart list is not defined by default (to prevent stopping them all) 34 | - name: (autostart) enable autostart for VMs in autostart list 35 | esxi_autostart: 36 | name: "{{ item.key}}" 37 | enabled: true 38 | order: "{{ item.value.order | default(omit) }}" 39 | with_dict: "{{ vms_to_autostart }}" 40 | when: 41 | - vms_to_autostart is defined 42 | - item.key in vm_info.id_by_vm 43 | # optional, to simplify startup 44 | - item.key not in vm_info.start_by_vm 45 | 46 | - name: (autostart) disable autostart for VMs not in autostart list 47 | esxi_autostart: 48 | name: "{{ item.key }}" 49 | enabled: false 50 | with_dict: "{{ vm_info.start_by_vm }}" 51 | when: 52 | - autostart_only_listed 53 | - item.key not in vms_to_autostart 54 | 55 | # todo: enable autostart for machines from list 56 | # - get list: `vim-cmd hostsvc/autostartmanager/get_autostartseq` 57 | # - not sure how to parse it w/o custom module 58 | # - get list of vm name to id mappings with `vim-cmd vmsvc/getallvms` 59 | # - compare and fix autostart entries 60 | # - most probably will need a module like "service" 61 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/network.yml: -------------------------------------------------------------------------------- 1 | - name: (network) get portgroups list 2 | # skip header (first 2 lines) and Management, print group, vswitch and vlan tag 3 | shell: "esxcli network vswitch standard portgroup list | awk -F' +' 'NR > 2 && !/^(Management Network) / {print $2, $4, $3, $1}'" 4 | register: portgroup_list_res 5 | changed_when: false 6 | check_mode: false 7 | 8 | - name: (network) convert portgroup list to structure 9 | set_fact: 10 | # steps: 11 | # - split to to array of tuples like ['adm-srv', 'vSwitch0', '210'] 12 | # - convert to records like {'name': 'adm-srv', 'switch': 'vSwitch0', 'tag': '210'} 13 | # - convert list of those records to dict keyed by name 14 | # - alt: replace "to_dict()" by "list" to get a list of records) 15 | # - result is structured like 'esxi_portgroups' from group_vars (keyed by name) 16 | portgroups: "{{ portgroup_list_res.stdout_lines 17 | | map('split', ' ', 3) 18 | | map('record', ['vswitch', 'tag', 'clients', 'name']) 19 | | to_dict('name') }}" 20 | pgmod: "esxcli network vswitch standard portgroup" 21 | 22 | - name: (network) add missed portgroups 23 | command: "{{ pgmod }} add -p '{{ item.key }}' -v {{ item.value.vswitch | d(vswitch_def) }}" 24 | with_dict: "{{ esxi_portgroups }}" 25 | when: item.key not in portgroups 26 | 27 | - name: (network) check that deleted portgroups are free from clients 28 | assert: 29 | that: "(portgroups[item].clients|int == 0)" 30 | with_items: "{{ portgroups.keys() }}" 31 | when: item not in esxi_portgroups 32 | 33 | - name: (network) delete extra portgroups 34 | command: "{{ pgmod }} remove -p '{{ item.key }}' -v {{ item.value.vswitch | d(vswitch_def) }}" 35 | with_dict: "{{ portgroups }}" 36 | when: (item.key not in esxi_portgroups) and (item.value.clients|int == 0) 37 | # loop_control does not work with "command" 38 | # loop_control: 39 | # label: "{{ item.key }}" 40 | 41 | - name: (network) set vlan tags to correct values 42 | command: "{{ pgmod }} set -p '{{ item.key }}' --vlan-id {{ item.value.tag }}" 43 | with_dict: "{{ esxi_portgroups }}" 44 | when: (item.key not in portgroups) or (item.value.tag != portgroups[item.key]['tag']|int) 45 | 46 | - name: (network) check if BPDUs are blocked 47 | shell: "esxcli system settings advanced list -o /Net/BlockGuestBPDU | awk '/^ Int Value:/ {print $3}'" 48 | register: bpdu_block_res 49 | changed_when: false 50 | check_mode: false 51 | 52 | - name: (network) block BPDUs from guests 53 | command: "esxcli system settings advanced set -o /Net/BlockGuestBPDU -i 1" 54 | when: 1 != bpdu_block_res.stdout|int 55 | 56 | - block: 57 | - name: (network) get ipv4 interfaces list 58 | command: "esxcli network ip interface ipv4 get" 59 | register: ipv4_ifaces_res 60 | changed_when: false 61 | check_mode: false 62 | 63 | - name: (network) parse ip interface list 64 | set_fact: 65 | # have 2 junk entries from header (with keys "Name" and "----") 66 | # safe to ignore (filter out with e.g. "rejectattr" if not) 67 | ip_by_nic: "{{ ipv4_ifaces_res.stdout_lines 68 | | map('split', None, 2) 69 | | map('record', ['name', 'address', 'rest']) 70 | | to_dict('name') }}" 71 | 72 | - name: (network) create interface for vMotion 73 | command: "esxcli network ip interface add -i {{ vmotion_iface_name }} -p '{{ vmotion_portgroup_name }}'" 74 | when: 75 | - vmotion_iface_name not in ipv4_ifaces_res.stdout 76 | 77 | - name: (network) calculate vmotion addr 78 | set_fact: 79 | # be careful not to include "." in RE: \1 became \1241 :) 80 | vmotion_addr: "{{ ip_by_nic['vmk0'].address | regex_replace('^(\\d+\\.\\d+)\\.\\d+', '\\1.' + (vmotion_subnet_number|string)) }}" 81 | 82 | - name: (network) set ip address on vMotion interface 83 | command: "esxcli network ip interface ipv4 set -i {{ vmotion_iface_name }} -I {{ vmotion_addr }} -N 255.255.255.0 -t static" 84 | when: 85 | - ip_by_nic[vmotion_iface_name]['address'] != vmotion_addr 86 | 87 | - name: (network) get tag on vMotion interface 88 | command: "esxcli network ip interface tag get -i {{ vmotion_iface_name }}" 89 | register: vmotion_tag_res 90 | changed_when: false 91 | check_mode: false 92 | 93 | - name: (network) assign vMotion interface tag 94 | command: "esxcli network ip interface tag add -i {{ vmotion_iface_name }} -t VMotion" 95 | when: 96 | - ('VMotion' not in vmotion_tag_res.stdout) 97 | 98 | when: create_vmotion_iface 99 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/users.yml: -------------------------------------------------------------------------------- 1 | - name: (users) get users list 2 | # skip header (first 2 lines), print rest (all fields) 3 | shell: "esxcli system account list | awk 'NR > 2 && !/^(root|dcui|vpxuser) / {print}'" 4 | register: users_list_res 5 | changed_when: false 6 | check_mode: false 7 | 8 | - name: (users) convert to structure 9 | set_fact: 10 | # trim ending spaces, convert to array of tuples like ['alex', 'his description'], make recods 11 | # from them like {'name': 'alex', 'descr': 'his description'}, convert list of those records 12 | # to dict keyed by name (alt: replace "to_dict()" by "list" to get a list of records) 13 | # reslut is structured like 'esxi_local_users' from group_vars (keyed by name) 14 | users: "{{ users_list_res.stdout_lines 15 | | map('trim') 16 | | map('split', None, 1) 17 | | map('record', ['name', 'desc']) 18 | | to_dict('name') }}" 19 | 20 | # Security.PasswordQualityControl cannot be set with esxcli; default is crazy in 6.5 21 | # or keep just one line in /etc/pam.d/passwd (remove "use_authtok") 22 | # password sufficient /lib/security/$ISA/pam_unix.so nullok shadow sha512 23 | - name: (users) allow any 8-char passwords 24 | lineinfile: 25 | dest: '/etc/pam.d/passwd' 26 | regexp: "^password +requisite.*pam_passwdqc" 27 | line: "password requisite /lib/security/$ISA/pam_passwdqc.so retry=3 min=8,8,8,8,8" 28 | 29 | - name: (users) add missed users 30 | command: > 31 | esxcli system account add --id={{ item.key }} --description='{{ item.value.desc }}' 32 | -p={{ lookup('password', 'creds/' + inventory_hostname + '.' + item.key + '.pass.out length=10 chars=ascii_letters,digits') }} 33 | -c={{ lookup('password', 'creds/' + inventory_hostname + '.' + item.key + '.pass.out length=10 chars=ascii_letters,digits') }} 34 | with_dict: "{{ esxi_local_users }}" 35 | register: added_users 36 | when: item.key not in users 37 | 38 | # actually wrong way to do it: need to set for all users, not only for changed 39 | # too lazy to fix now :) 40 | - name: (users) set privs for added users 41 | # changes rights to full admin: DCUI login of shutdown (F12), console shell with alt-F1, 42 | # ssh access too; not sure how to restrict that with standalone esxi 43 | command: "esxcli system permission set -i={{ item.item.key }} -r=Admin" 44 | with_items: "{{ added_users.results }}" 45 | when: item.changed 46 | # no_log: true 47 | 48 | - name: (users) print out temp passwords for added users 49 | debug: 50 | msg: "temp password for {{ item }}: {{ lookup('password', 'creds/' + inventory_hostname + '.' + item + '.pass.out') }}" 51 | with_items: "{{ added_users.results | selectattr('changed') | map(attribute='item') | map(attribute='key') | list }}" 52 | 53 | - name: (users) modify settings for changed users 54 | command: "esxcli system account set --id={{ item.key }} --description='{{ esxi_local_users[item.key]['desc'] }}'" 55 | with_dict: "{{ users }}" 56 | when: 57 | - esxi_local_users[item.key] is defined 58 | - esxi_local_users[item.key]['desc'] != item.value.desc 59 | 60 | - name: (users) delete extra users 61 | command: "esxcli system account remove --id={{ item.key }}" 62 | with_dict: "{{ users }}" 63 | when: item.key not in esxi_local_users 64 | 65 | - name: (users) generate ssh key restoration script 66 | template: 67 | src: "gen_keys.sh.j2" 68 | dest: "/etc/rc.local.d/local.sh" 69 | mode: "u=rwx,og=r" 70 | register: genkeys_script_res 71 | 72 | - name: (users) run script to regenerate keys 73 | shell: "/etc/rc.local.d/local.sh" 74 | when: genkeys_script_res.changed 75 | 76 | - name: (users) copy profile 77 | copy: 78 | src: "profile.local" 79 | dest: "/etc/profile.local" 80 | mode: "u=rwx,og=r" 81 | 82 | - name: (users) check ssh timeout 83 | shell: "esxcli system settings advanced list -o /UserVars/ESXiShellInteractiveTimeOut | awk '/^ Int Value:/ {print $3}'" 84 | register: ssh_timeout_res 85 | changed_when: false 86 | check_mode: false 87 | 88 | - name: (users) set ssh timeout 89 | command: "esxcli system settings advanced set -o /UserVars/ESXiShellInteractiveTimeOut -i {{ ssh_timeout }}" 90 | # explicitly converting to int 91 | when: ssh_timeout != ssh_timeout_res.stdout|int 92 | 93 | - name: (users) check ssh client ruleset state 94 | command: "esxcli network firewall ruleset list --ruleset-id=sshClient" 95 | register: ssh_ruleset_state 96 | changed_when: false 97 | check_mode: false 98 | 99 | - name: (users) enable ssh client through firewall 100 | command: "esxcli network firewall ruleset set --ruleset-id=sshClient --enabled=true" 101 | when: ssh_ruleset_state.stdout.find("false") != -1 102 | -------------------------------------------------------------------------------- /roles/hostconf-esxi/tasks/storage.yml: -------------------------------------------------------------------------------- 1 | # rename "datastore1" -> "(hostname)-sys" 2 | 3 | - name: (storage) get filesystems list 4 | # also: "esxcli storage vmfs extent list" 5 | shell: "esxcfg-scsidevs --vmfs | grep -v OSDATA | awk '{print $1, $5}' | sed -e 's/:/ /'" 6 | register: fs_list_res 7 | changed_when: false 8 | check_mode: false 9 | 10 | - name: (storage) get device paths list 11 | # several paths could map to one device 12 | shell: "esxcfg-mpath -L | awk '{print $1, $3, $4, $5, $6, $7}'" 13 | register: dev_list_res 14 | changed_when: false 15 | check_mode: false 16 | 17 | - name: (storage) get filesystems usage 18 | # a bit complex :) 19 | shell: "for f in $(esxcli storage filesystem list|grep VMFS|sed 's/ */ /g'|awk -F' ' '{print $2}'); do echo -n $f '' && ls /vmfs/volumes/$f/|wc -l;done" 20 | register: fs_usage_res 21 | changed_when: false 22 | check_mode: false 23 | 24 | - name: (storage) convert lists to structures 25 | set_fact: 26 | # steps: 27 | # - split to to array of tuples like ['adm-srv', 'vSwitch0', '210'] 28 | # - convert to records like {'name': 'adm-srv', 'switch': 'vSwitch0', 'tag': '210'} 29 | # - convert list of those records to dict keyed by name 30 | # - alt: replace "to_dict()" by "list" to get a list of records) 31 | # - result is structured like 'esxi_portgroups' from group_vars (keyed by name) 32 | fsinfo_by_dev: "{{ fs_list_res.stdout_lines 33 | | map('split', ' ', 2) 34 | | map('record', ['dev', 'part', 'name']) 35 | | to_dict('dev') 36 | }}" 37 | devinfo_by_path: "{{ dev_list_res.stdout_lines 38 | | map('split', ' ', 6) 39 | | map('record', ['path', 'dev', 'hba', 'ctr', 'tgt', 'lun']) 40 | | to_dict('path') 41 | }}" 42 | usage_by_fs: "{{ fs_usage_res.stdout_lines 43 | | map('split', ' ', 1) 44 | | map('record', ['name', 'usage']) 45 | | to_dict('name') 46 | }}" 47 | 48 | #- name: print fs 49 | # debug: 50 | # var: fsinfo_by_dev 51 | #- name: print dev 52 | # debug: 53 | # var: devinfo_by_path 54 | #- name: print usage 55 | # debug: 56 | # var: usage_by_fs 57 | 58 | # delete extra files manually if required 59 | - name: (storage) check that all wrongly named datastores are empty 60 | assert: 61 | that: 62 | - usage_by_fs[fsinfo_by_dev[devinfo_by_path[item.key].dev].name].usage == "0" 63 | msg: "cannot rename datastore {{ item.key }} from {{ fsinfo_by_dev[devinfo_by_path[item.key].dev].name }} to {{ item.value }}: not empty" 64 | with_dict: "{{ local_datastores }}" 65 | when: 66 | - rename_datastores == true 67 | - devinfo_by_path[item.key].dev in fsinfo_by_dev 68 | - fsinfo_by_dev[devinfo_by_path[item.key].dev].name != item.value 69 | 70 | - name: (storage) rename datastores 71 | shell: "vim-cmd hostsvc/datastore/rename {{ fsinfo_by_dev[devinfo_by_path[item.key]['dev']]['name'] }} {{ item.value }}" 72 | with_dict: "{{ local_datastores }}" 73 | when: 74 | - rename_datastores == true 75 | - devinfo_by_path[item.key].dev in fsinfo_by_dev 76 | - fsinfo_by_dev[devinfo_by_path[item.key].dev].name != item.value 77 | - usage_by_fs[fsinfo_by_dev[devinfo_by_path[item.key].dev].name].usage == "0" 78 | 79 | - name: (storage) show missed datastores on vacant LUNs 80 | debug: 81 | msg: "will partition and newfs {{ item.key }} ({{ devinfo_by_path[item.key].dev }})" 82 | with_dict: "{{ local_datastores }}" 83 | when: 84 | - create_datastores == true 85 | - devinfo_by_path[item.key].dev not in fsinfo_by_dev 86 | 87 | # better check that size is correct :) 88 | # on errors like "unknown partition table": "partedUtil mklabel /dev/disks/naa.NNN gpt" 89 | # on errors like "not all space is used": try "partedUtil fix /dev/disks/naa.NNN" 90 | - name: (storage) make sure partition tables on vacant LUNs are empty 91 | shell: "[[ $(partedUtil getptbl /dev/disks/{{ devinfo_by_path[item.key].dev }} | wc -l) == 2 ]]" 92 | with_dict: "{{ local_datastores }}" 93 | when: 94 | - create_datastores == true 95 | - devinfo_by_path[item.key].dev not in fsinfo_by_dev 96 | check_mode: false 97 | changed_when: false 98 | 99 | - name: (storage) create gpt labels on vacant LUNs 100 | shell: "partedUtil mklabel /dev/disks/{{ devinfo_by_path[item.key].dev }} gpt" 101 | with_dict: "{{ local_datastores }}" 102 | when: 103 | - create_datastores == true 104 | - devinfo_by_path[item.key].dev not in fsinfo_by_dev 105 | 106 | - name: (storage) partition vacant LUNs 107 | shell: > 108 | dev=/dev/disks/{{ devinfo_by_path[item.key].dev }}; 109 | last=$(partedUtil getUsableSectors $dev | awk '{print $2}'); 110 | partedUtil setptbl $dev gpt "1 128 $last {{ vmfs_guid }} 0" 111 | with_dict: "{{ local_datastores }}" 112 | when: 113 | - create_datastores == true 114 | - devinfo_by_path[item.key].dev not in fsinfo_by_dev 115 | 116 | - name: (storage) create VMFS on vacant LUNs 117 | shell: "vmkfstools -C vmfs5 -b 1m -S {{ item.value }} /dev/disks/{{ devinfo_by_path[item.key].dev }}:1" 118 | with_dict: "{{ local_datastores }}" 119 | when: 120 | - create_datastores == true 121 | - devinfo_by_path[item.key].dev not in fsinfo_by_dev 122 | 123 | # todo: make LUNs with smartarray: too lazy to parse ssacli output now 124 | -------------------------------------------------------------------------------- /library/esxi_vib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | source ~/tmp/ansible/hacking/env-setup 4 | export PATH=$PATH:~/tmp/ansible/hacking/ 5 | test-module -m esxi_vib.py -a 'mock=yes name=esx-ui state=present' 6 | 7 | export ANSIBLE_CONFIG=~/works/sysadm/ansible-study/esxi-mgmt/ansible.esxi.cfg 8 | ansible -m esxi_vib 'name=esx-ui state=present' --check nest1-m8 9 | ''' 10 | 11 | from ansible.module_utils.basic import AnsibleModule 12 | 13 | ANSIBLE_METADATA = {'status': ['preview'], 14 | 'supported_by': 'committer', 15 | 'version': '0.1'} 16 | 17 | DOCUMENTATION = ''' 18 | --- 19 | module: esxi_vib 20 | short_description: manage VIB package installation on ESXi 21 | version_added: "2.2" 22 | description: 23 | - Manages installation, update and deinstallation of VIB packages on ESXi hosts. 24 | options: 25 | name: 26 | description: VIB package name 27 | required: true 28 | url: 29 | description: http url to package to install or update 30 | state: 31 | description: 32 | - C(present) will make sure the package is installed. 33 | C(latest) will make sure the latest version of the package is installed. 34 | C(absent) will make sure the specified package is not installed. 35 | required: false 36 | choices: [ present, latest, absent ] 37 | default: "present" 38 | author: alex@maxidom.ru 39 | notes: 40 | - works w/o vcenter via C(ssh) 41 | requirements: 42 | - none 43 | ''' 44 | 45 | EXAMPLES = ''' 46 | # install ESXi Embedded Host Client 47 | - name: inst host client 48 | esxi_vib: 49 | name: esx-ui 50 | url: http://distr.internal/vibs/esxui-signed-4974903.vib 51 | 52 | ''' 53 | 54 | 55 | def parse_cmd_responce(lines, skip_empty = True): 56 | ''' parse multiline responce from "esxcli software vib", looking like 57 | 58 | VMware_bootbank_esx-ui_0.0.2-0.1.3172496 59 | Name: esx-ui 60 | Version: 0.0.2-0.1.3172496 61 | 62 | and extracts interesting attrs, mapping name to shorter version 63 | mb using "esxcli --formatter=xml" would be wiser :) 64 | ''' 65 | res = dict() 66 | title = None 67 | for line in lines.split('\n'): 68 | if title is None: 69 | title = line 70 | elif line.startswith(' '): 71 | key, val = line.lstrip().split(":", 1) 72 | vals = val.lstrip() 73 | if not (skip_empty and vals == ''): 74 | res[key] = val.lstrip() 75 | 76 | res['Title'] = title 77 | return res 78 | 79 | 80 | def get_vib_state(module, vib_name, skip_empty = True): 81 | ''' gets current state of VIB (installed or not) and version if installed''' 82 | # also: 'esxcli software profile get' to view installed 83 | ret, out, err = module.run_command("esxcli software vib get -n %s" % vib_name) 84 | if ret != 0: 85 | if out.lstrip().startswith('[NoMatchError]'): 86 | return "absent", None 87 | module.fail_json(msg="unable to get vib info", rc=ret, err=err, out=out) 88 | details = parse_cmd_responce(out, skip_empty) 89 | if 'Version' in details: 90 | return "present", details 91 | else: 92 | module.fail_json(msg="package %s is neither present nor absent" % vib_name, out=out, err=err) 93 | 94 | 95 | def main(): 96 | ''' entry point, simple one for now 97 | run with test-module -m esxi_vib.py -a "name=hren" 98 | ''' 99 | module = AnsibleModule( 100 | argument_spec = dict( 101 | name = dict(required=True), 102 | state = dict(required=False, default='present', choices=['present', 'latest', 'absent']), 103 | url = dict(required=False) 104 | ), 105 | supports_check_mode=True, 106 | ) 107 | vib_name = module.params['name'] 108 | vib_url = module.params['url'] 109 | state_new = module.params['state'] 110 | state_curr, details_curr = get_vib_state(module, vib_name) 111 | command = None 112 | action = None 113 | if state_new == 'absent': 114 | if state_curr != 'absent': 115 | command = "esxcli software vib remove -n {0}".format(vib_name) 116 | action = 'remove' 117 | elif state_new == 'present': 118 | if state_curr != 'present': 119 | command = "esxcli software vib install -v {0}".format(vib_url) 120 | action = 'install' 121 | elif state_new == 'latest': 122 | if state_curr == 'present': 123 | command = "esxcli software vib update -v {0}".format(vib_url) 124 | action = 'update' 125 | else: 126 | command = "esxcli software vib install -v {0}".format(vib_url) 127 | action = 'install' 128 | else: 129 | module.fail_json(msg="unknown new state %s" % state_new) 130 | 131 | if action is None: 132 | module.exit_json(changed=False, msg="already ok: %s" % state_curr, details = details_curr) 133 | 134 | full_cmd = command + (" --dry-run" if module.check_mode else '') 135 | ret, out, err = module.run_command(full_cmd) 136 | if ret != 0: 137 | # "vib update" sometimes fail with empty message, but actual result is ok 138 | if action == 'update' and ret == 1 and err == "" and out == "''\n": 139 | ret2, out2, err2 = module.run_command(full_cmd) 140 | state_new, details_new = get_vib_state(module, vib_name) 141 | if details_curr['Version'] != details_new['Version']: 142 | module.exit_json(changed = True, msg="update finished mostly ok :)") 143 | else: 144 | module.exit_json(changed = False, msg="update skipped mostly ok :)") 145 | else: 146 | module.fail_json(msg="command failed", cmd=full_cmd, rc=ret, err=err, out=out) 147 | res_details = parse_cmd_responce(out) 148 | changed = ('VIBs Installed' in res_details or 'VIBs Removed' in res_details) 149 | module.exit_json(changed=changed, command=full_cmd, details=res_details) 150 | 151 | if __name__ == '__main__': 152 | main() 153 | -------------------------------------------------------------------------------- /library/esxi_vm_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | source ~/tmp/ansible/hacking/env-setup 4 | export PATH=$PATH:~/tmp/ansible/hacking/ 5 | test-module -m esxi_vm_list.py -a 'mock=yes' 6 | 7 | export ANSIBLE_CONFIG=~/works/sysadm/ansible-study/esxi-mgmt/ansible.esxi.cfg 8 | ansible -m esxi_vm_list nest1-m8 9 | ''' 10 | 11 | from ansible.module_utils.basic import AnsibleModule 12 | import re 13 | 14 | ANSIBLE_METADATA = {'status': ['preview'], 15 | 'supported_by': 'committer', 16 | 'version': '0.1'} 17 | 18 | DOCUMENTATION = ''' 19 | --- 20 | module: esx_vm_info 21 | short_description: list registered VMs and their properties for stand-alone ESXi host 22 | version_added: "2.2" 23 | description: 24 | - 'This module lists VMs on ESXi hosts, returing serveral dicts.' 25 | - 'Basic info include C(vm_by_id) (maps VM id => VM name) and C(id_by_vm) 26 | (maps VM name => VM id (int)).' 27 | - 'Optional are C(start_by_vm) (VM name => autostart sequence number, absent 28 | if VM is not started automatically) and C(power_by_vm) (VM name => current power 29 | state, C(true) if powered on).' 30 | options: 31 | get_start_state: 32 | description: include C(start_by_vm) dictionary with startup sequence 33 | default: False 34 | get_power_state: 35 | description: 36 | - include C(power_by_vm) dictionary with power state. 37 | - C(true) means VM is currently powered on. 38 | - Takes longer to calculate. 39 | default: False 40 | requirements: [] 41 | author: alex@maxidom.ru 42 | notes: 43 | - works w/o vcenter via C(ssh) 44 | - VM id is string as C(vm_by_id) key because C(int) could not be a key in JSON 45 | ''' 46 | 47 | EXAMPLES = ''' 48 | 49 | # get basic vm info 50 | - name: get vm info 51 | esxi_vm_info: 52 | register: vminfo 53 | 54 | # use it to find if VM is registered 55 | - name: fix autostart for registered VMs 56 | esxi_autostart: 57 | name: "{{ item.name }}" 58 | start: "{{ item.start }}" 59 | order: "{{ item.order | default(omit) }}" 60 | with_items: "{{ vms_to_start }}" 61 | when: item.name in vminfo.id_by_vm 62 | ''' 63 | 64 | MOCK_DIR = '/Users/alex/works/sysadm/ansible-study/esxi-mgmt/experiments/mocks' 65 | 66 | 67 | def load_vm_list(module): 68 | ''' construct map "vm_name -> vm_id" from file or program ''' 69 | id_by_vm = dict() 70 | vm_by_id = dict() 71 | path_by_vm = dict() 72 | # ret, out, err = module.run_command('cat %s/getallvms.txt' % MOCK_DIR) 73 | ret, out, err = module.run_command('vim-cmd vmsvc/getallvms') 74 | if ret != 0: 75 | module.fail_json(msg="unable to get vm list", rc=ret, err=err) 76 | for line in out.split('\n'): 77 | if line.startswith('Vmid') or line == '': 78 | continue 79 | # multiline annotations are tricky 80 | lparts = re.match(r'^(?P\d+) +(?P\S+) +\[(?P\S+)\] (?P\S+)/(?P\S+)\.vmx ', line) 81 | if not lparts: 82 | continue 83 | id_by_vm[lparts.group("name")] = int(lparts.group("id")) 84 | vm_by_id[lparts.group("id")] = lparts.group("name") 85 | path_by_vm[lparts.group("name")] = "/vmfs/volumes/" + lparts.group("store") + "/" + lparts.group("path") + "/" + lparts.group("file") + ".vmx" 86 | return vm_by_id, id_by_vm, path_by_vm 87 | 88 | 89 | def load_startup_list(module, vm_by_id): 90 | ''' 91 | construct map "vm_name -> autostart_order" for autostart 92 | if machine is not in list 93 | ''' 94 | sinfo = dict() 95 | vm_name = '' 96 | vm_enabled = False 97 | vm_order = 0 98 | # for line in file(STARTUP_FILE): 99 | # or subprocess.Popen + res.stdout.readlines 100 | # for line in os.popen("cat %s" % STARTUP_FILE).readlines(): 101 | ret, out, err = module.run_command('vim-cmd hostsvc/autostartmanager/get_autostartseq') 102 | if ret != 0: 103 | module.fail_json(msg="unable go get startup list", rc=ret, err=err) 104 | for line in out.split('\n'): 105 | if line.lstrip().startswith(('(', '}', ']')) or line == '': 106 | continue 107 | (key, _, val) = line.strip("', \n").strip().split() 108 | if key == 'key': 109 | # key = 'vim.VirtualMachine:3', 110 | vm_name = vm_by_id[val.split(":")[1]] 111 | vm_enabled = False 112 | vm_order = 0 113 | elif key == 'startOrder': 114 | vm_order = int(val) 115 | if vm_enabled: 116 | sinfo[vm_name] = vm_order 117 | #sinfo[vm_name] = vm_order 118 | elif key == 'startAction': 119 | # could be 'PowerOn' and 'powerOn' 120 | if str.lower(val.strip('"')) == 'poweron': 121 | vm_enabled = True 122 | if vm_order > 0: 123 | sinfo[vm_name] = vm_order 124 | else: 125 | vm_enabled = False 126 | return sinfo 127 | 128 | 129 | def load_power_list(module, vm_by_id): 130 | ''' 131 | make map "vm_name -> power_state 132 | ''' 133 | pinfo = dict() 134 | for (vm_id, vm_name) in vm_by_id.items(): 135 | ret, out, err = module.run_command('vim-cmd vmsvc/power.getstate %s' % vm_id) 136 | if out.endswith("on\n"): 137 | pinfo[vm_name] = True 138 | else: 139 | pinfo[vm_name] = False 140 | return pinfo 141 | 142 | def main(): 143 | ''' entry point, simple one for now 144 | run mock: test-module -m esxi_vm_list.py 145 | run real: ansible -m esxi_vm_list nest1-m8 146 | ''' 147 | module = AnsibleModule( 148 | argument_spec = dict( 149 | get_start_state = dict(required=False, type='bool', default=False), 150 | get_power_state = dict(required=False, type='bool', default=False), 151 | ), 152 | supports_check_mode=True, 153 | ) 154 | # module.debug('stated') 155 | # mgr = VMStartMgr(module) 156 | ret_dict = dict() 157 | vm_by_id, id_by_vm, path_by_vm = load_vm_list(module) 158 | ret_dict['vm_by_id'] = vm_by_id 159 | ret_dict['id_by_vm'] = id_by_vm 160 | ret_dict['path_by_vm'] = path_by_vm 161 | if module.params['get_start_state']: 162 | ret_dict['start_by_vm'] = load_startup_list(module, vm_by_id) 163 | if module.params['get_power_state']: 164 | ret_dict['power_by_vm'] = load_power_list(module, vm_by_id) 165 | module.exit_json(changed = False, **ret_dict) 166 | 167 | if __name__ == '__main__': 168 | main() 169 | -------------------------------------------------------------------------------- /vm_deploy/replace.py-2.2.orig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2013, Evan Kaufman . 20 | 21 | import re 22 | import os 23 | import tempfile 24 | 25 | DOCUMENTATION = """ 26 | --- 27 | module: replace 28 | author: "Evan Kaufman (@EvanK)" 29 | extends_documentation_fragment: 30 | - files 31 | - validate 32 | short_description: Replace all instances of a particular string in a 33 | file using a back-referenced regular expression. 34 | description: 35 | - This module will replace all instances of a pattern within a file. 36 | - It is up to the user to maintain idempotence by ensuring that the 37 | same pattern would never match any replacements made. 38 | version_added: "1.6" 39 | options: 40 | dest: 41 | required: true 42 | aliases: [ name, destfile ] 43 | description: 44 | - The file to modify. 45 | regexp: 46 | required: true 47 | description: 48 | - The regular expression to look for in the contents of the file. 49 | Uses Python regular expressions; see 50 | U(http://docs.python.org/2/library/re.html). 51 | Uses multiline mode, which means C(^) and C($) match the beginning 52 | and end respectively of I(each line) of the file. 53 | replace: 54 | required: false 55 | description: 56 | - The string to replace regexp matches. May contain backreferences 57 | that will get expanded with the regexp capture groups if the regexp 58 | matches. If not set, matches are removed entirely. 59 | backup: 60 | required: false 61 | default: "no" 62 | choices: [ "yes", "no" ] 63 | description: 64 | - Create a backup file including the timestamp information so you can 65 | get the original file back if you somehow clobbered it incorrectly. 66 | others: 67 | description: 68 | - All arguments accepted by the M(file) module also work here. 69 | required: false 70 | follow: 71 | required: false 72 | default: "no" 73 | choices: [ "yes", "no" ] 74 | version_added: "1.9" 75 | description: 76 | - 'This flag indicates that filesystem links, if they exist, should be followed.' 77 | """ 78 | 79 | EXAMPLES = r""" 80 | - replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes 81 | 82 | - replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644 83 | 84 | - replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t' 85 | """ 86 | 87 | def write_changes(module,contents,dest): 88 | 89 | tmpfd, tmpfile = tempfile.mkstemp() 90 | f = os.fdopen(tmpfd,'wb') 91 | f.write(contents) 92 | f.close() 93 | 94 | validate = module.params.get('validate', None) 95 | valid = not validate 96 | if validate: 97 | if "%s" not in validate: 98 | module.fail_json(msg="validate must contain %%s: %s" % (validate)) 99 | (rc, out, err) = module.run_command(validate % tmpfile) 100 | valid = rc == 0 101 | if rc != 0: 102 | module.fail_json(msg='failed to validate: ' 103 | 'rc:%s error:%s' % (rc,err)) 104 | if valid: 105 | module.atomic_move(tmpfile, dest, unsafe_writes=module.params['unsafe_writes']) 106 | 107 | def check_file_attrs(module, changed, message): 108 | 109 | file_args = module.load_file_common_arguments(module.params) 110 | if module.set_file_attributes_if_different(file_args, False): 111 | 112 | if changed: 113 | message += " and " 114 | changed = True 115 | message += "ownership, perms or SE linux context changed" 116 | 117 | return message, changed 118 | 119 | def main(): 120 | module = AnsibleModule( 121 | argument_spec=dict( 122 | dest=dict(required=True, aliases=['name', 'destfile']), 123 | regexp=dict(required=True), 124 | replace=dict(default='', type='str'), 125 | backup=dict(default=False, type='bool'), 126 | validate=dict(default=None, type='str'), 127 | ), 128 | add_file_common_args=True, 129 | supports_check_mode=True 130 | ) 131 | 132 | params = module.params 133 | dest = os.path.expanduser(params['dest']) 134 | diff = dict() 135 | 136 | if os.path.isdir(dest): 137 | module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) 138 | 139 | if not os.path.exists(dest): 140 | module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) 141 | else: 142 | f = open(dest, 'rb') 143 | contents = f.read() 144 | f.close() 145 | 146 | if module._diff: 147 | diff = { 148 | 'before_header': dest, 149 | 'before': contents, 150 | } 151 | 152 | mre = re.compile(params['regexp'], re.MULTILINE) 153 | result = re.subn(mre, params['replace'], contents, 0) 154 | 155 | if result[1] > 0 and contents != result[0]: 156 | msg = '%s replacements made' % result[1] 157 | changed = True 158 | if module._diff: 159 | diff['after_header'] = dest 160 | diff['after'] = result[0] 161 | else: 162 | msg = '' 163 | changed = False 164 | diff = dict() 165 | 166 | if changed and not module.check_mode: 167 | if params['backup'] and os.path.exists(dest): 168 | module.backup_local(dest) 169 | if params['follow'] and os.path.islink(dest): 170 | dest = os.path.realpath(dest) 171 | write_changes(module, result[0], dest) 172 | 173 | msg, changed = check_file_attrs(module, changed, msg) 174 | module.exit_json(changed=changed, msg=msg, diff=diff) 175 | 176 | # this is magic, see lib/ansible/module_common.py 177 | from ansible.module_utils.basic import * 178 | 179 | if __name__ == '__main__': 180 | main() 181 | -------------------------------------------------------------------------------- /vm_deploy/replace.py-2.2_fixed_for_python3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # place to ./lib/python2.7/site-packages/ansible/modules/core/files/replace.py 5 | 6 | # (c) 2013, Evan Kaufman . 22 | 23 | import re 24 | import os 25 | import tempfile 26 | 27 | from ansible.module_utils._text import to_text, to_bytes 28 | 29 | DOCUMENTATION = """ 30 | --- 31 | module: replace 32 | author: "Evan Kaufman (@EvanK)" 33 | extends_documentation_fragment: 34 | - files 35 | - validate 36 | short_description: Replace all instances of a particular string in a 37 | file using a back-referenced regular expression. 38 | description: 39 | - This module will replace all instances of a pattern within a file. 40 | - It is up to the user to maintain idempotence by ensuring that the 41 | same pattern would never match any replacements made. 42 | version_added: "1.6" 43 | options: 44 | dest: 45 | required: true 46 | aliases: [ name, destfile ] 47 | description: 48 | - The file to modify. 49 | regexp: 50 | required: true 51 | description: 52 | - The regular expression to look for in the contents of the file. 53 | Uses Python regular expressions; see 54 | U(http://docs.python.org/2/library/re.html). 55 | Uses multiline mode, which means C(^) and C($) match the beginning 56 | and end respectively of I(each line) of the file. 57 | replace: 58 | required: false 59 | description: 60 | - The string to replace regexp matches. May contain backreferences 61 | that will get expanded with the regexp capture groups if the regexp 62 | matches. If not set, matches are removed entirely. 63 | backup: 64 | required: false 65 | default: "no" 66 | choices: [ "yes", "no" ] 67 | description: 68 | - Create a backup file including the timestamp information so you can 69 | get the original file back if you somehow clobbered it incorrectly. 70 | others: 71 | description: 72 | - All arguments accepted by the M(file) module also work here. 73 | required: false 74 | follow: 75 | required: false 76 | default: "no" 77 | choices: [ "yes", "no" ] 78 | version_added: "1.9" 79 | description: 80 | - 'This flag indicates that filesystem links, if they exist, should be followed.' 81 | """ 82 | 83 | EXAMPLES = r""" 84 | - replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes 85 | 86 | - replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644 87 | 88 | - replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t' 89 | """ 90 | 91 | def write_changes(module,contents,dest): 92 | 93 | tmpfd, tmpfile = tempfile.mkstemp() 94 | f = os.fdopen(tmpfd,'wb') 95 | f.write(to_bytes(contents)) 96 | f.close() 97 | 98 | validate = module.params.get('validate', None) 99 | valid = not validate 100 | if validate: 101 | if "%s" not in validate: 102 | module.fail_json(msg="validate must contain %%s: %s" % (validate)) 103 | (rc, out, err) = module.run_command(validate % tmpfile) 104 | valid = rc == 0 105 | if rc != 0: 106 | module.fail_json(msg='failed to validate: ' 107 | 'rc:%s error:%s' % (rc,err)) 108 | if valid: 109 | module.atomic_move(tmpfile, dest, unsafe_writes=module.params['unsafe_writes']) 110 | 111 | def check_file_attrs(module, changed, message): 112 | 113 | file_args = module.load_file_common_arguments(module.params) 114 | if module.set_file_attributes_if_different(file_args, False): 115 | 116 | if changed: 117 | message += " and " 118 | changed = True 119 | message += "ownership, perms or SE linux context changed" 120 | 121 | return message, changed 122 | 123 | def main(): 124 | module = AnsibleModule( 125 | argument_spec=dict( 126 | dest=dict(required=True, aliases=['name', 'destfile']), 127 | regexp=dict(required=True), 128 | replace=dict(default='', type='str'), 129 | backup=dict(default=False, type='bool'), 130 | validate=dict(default=None, type='str'), 131 | ), 132 | add_file_common_args=True, 133 | supports_check_mode=True 134 | ) 135 | 136 | params = module.params 137 | dest = os.path.expanduser(params['dest']) 138 | diff = dict() 139 | 140 | if os.path.isdir(dest): 141 | module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) 142 | 143 | if not os.path.exists(dest): 144 | module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) 145 | else: 146 | f = open(dest, 'rb') 147 | contents = to_text(f.read(), errors='surrogate_or_strict') 148 | f.close() 149 | 150 | if module._diff: 151 | diff = { 152 | 'before_header': dest, 153 | 'before': contents, 154 | } 155 | 156 | mre = re.compile(params['regexp'], re.MULTILINE) 157 | result = re.subn(mre, params['replace'], contents, 0) 158 | 159 | if result[1] > 0 and contents != result[0]: 160 | msg = '%s replacements made' % result[1] 161 | changed = True 162 | if module._diff: 163 | diff['after_header'] = dest 164 | diff['after'] = result[0] 165 | else: 166 | msg = '' 167 | changed = False 168 | diff = dict() 169 | 170 | if changed and not module.check_mode: 171 | if params['backup'] and os.path.exists(dest): 172 | module.backup_local(dest) 173 | if params['follow'] and os.path.islink(dest): 174 | dest = os.path.realpath(dest) 175 | write_changes(module, result[0], dest) 176 | 177 | msg, changed = check_file_attrs(module, changed, msg) 178 | module.exit_json(changed=changed, msg=msg, diff=diff) 179 | 180 | # this is magic, see lib/ansible/module_common.py 181 | from ansible.module_utils.basic import * 182 | 183 | if __name__ == '__main__': 184 | main() 185 | -------------------------------------------------------------------------------- /update_esxi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # playbook to update standalone (or vcentered) with offline bundle; will 3 | # - download it from http server to temp dir 4 | # - check if profile has something to apply 5 | # - apply it if not 6 | # - reboot host if esx-base is updated and allow_reboot=true 7 | # - wait for host to came back online 8 | # - remove temp file afterwards 9 | 10 | # export ANSIBLE_CONFIG=/Users/alex/works/sysadm/ansible-study/esxi-mgmt/ansible.esxi.cfg 11 | # ansible-playbook update_esxi.yaml -l nest-test 12 | # -e 'bundle=VMware-ESXi-6.5.0-Update1-5969303-HPE-650.U1.10.1.0.14-Jul2017-depot.zip' \ 13 | # -e 'src=http://www-distr.m1.maxidom.ru/suse_distr/iso' \ 14 | # -e 'build=5969303' 15 | # [ -e 'force_reboot=true'] 16 | 17 | # args and defaults: 18 | # - only required arg is offline patch bundle name 19 | # - default params are in update_esxi_defaults.yaml and for as they are 20 | # - server: jackdaw.m1.maxidom.ru 21 | # - path: /suse_distr/iso 22 | # - temp datastore: 1st defined in host conf, or hostname + "-sys" 23 | # - reboot: if required by patch and no VMs are currently running 24 | 25 | # check mode is not truly supported (yet?): mb it will be ok just to download 26 | # patch and dry-run install w/o actual installation 27 | 28 | - hosts: all 29 | 30 | # mostly from defaults (included later, but its ok) 31 | vars: 32 | bundle_url: "{{ src | default(default_http_src) }}/{{ bundle }}" 33 | temp_path: "{{ '/vmfs/volumes/' + temp_dir|default(default_temp_dir) }}" 34 | 35 | tasks: 36 | 37 | - include_vars: "update_esxi_defaults.yaml" 38 | 39 | - name: check that patch bundle name is provided 40 | assert: 41 | that: 42 | - bundle is defined 43 | msg: "please specify at least -e bundle=" 44 | 45 | # really optional; could be multi 46 | - name: check that play targets exactly one esxi host 47 | assert: 48 | that: 49 | - ansible_play_hosts|length == 1 50 | - ansible_os_family == "VMkernel" 51 | msg: "please target exactly one vmware host with this play" 52 | 53 | - name: warn that remote kernel is already current 54 | debug: 55 | msg: "remote kernel is already at build {{ build }}, will not continue" 56 | when: 57 | - build is defined 58 | - ansible_distribution_version.startswith("#1 SMP Release build-" + build ) 59 | 60 | # will silently abort execution if kernel is already ok 61 | - meta: end_play 62 | when: 63 | - build is defined 64 | - ansible_distribution_version.startswith("#1 SMP Release build-" + build ) 65 | 66 | - name: get list of running VMs 67 | esxi_vm_info: 68 | get_power_state: true 69 | register: vm_info_res 70 | 71 | - name: check if reboot is possible (no running VMs) 72 | set_fact: 73 | reboot_possible: "{{ vm_info_res.power_by_vm | select | list | count == 0 }}" 74 | 75 | - name: check that host is either free of VMs or reboot is forced 76 | assert: 77 | that: 78 | - reboot_possible or force_reboot 79 | msg: "please either stop/migrate running VMs or use -e 'force_reboot=true'" 80 | 81 | - name: make sure that temp path exists 82 | stat: 83 | path: "{{ temp_path }}" 84 | register: temp_path_res 85 | failed_when: not temp_path_res.stat.exists 86 | 87 | - name: fetch patch bundle to temp path 88 | get_url: 89 | url: "{{ bundle_url }}" 90 | dest: "{{ temp_path }}" 91 | tmp_dest: "{{ temp_path }}" 92 | 93 | - name: list profiles in bundle 94 | shell: "esxcli software sources profile list -d {{ temp_path }}/{{ bundle }} | awk 'NR>2 {print $1}'" 95 | register: profile_res 96 | failed_when: profile_res.stdout_lines | count != 1 97 | changed_when: false 98 | 99 | - name: dry-run software install 100 | shell: > 101 | esxcli --formatter=keyvalue software profile update 102 | -p {{ profile_res.stdout }} 103 | -d {{ temp_path }}/{{bundle }} 104 | --dry-run 105 | register: update_test_res 106 | changed_when: >- 107 | not (update_test_res.stdout_lines[0].endswith('The following installers will be applied: []') 108 | and update_test_res.stdout_lines[1].endswith('RebootRequired.boolean=false') 109 | and update_test_res.stdout_lines[2].endswith('VIBsInstalled.string[] = ') 110 | and update_test_res.stdout_lines[3].endswith('VIBsRemoved.string[] = ')) 111 | 112 | - name: perform install if required 113 | shell: > 114 | esxcli --formatter=keyvalue software profile update 115 | -p {{ profile_res.stdout }} 116 | -d {{ temp_path }}/{{bundle }} 117 | register: update_res 118 | changed_when: | 119 | not (update_res.stdout_lines[0].endswith('The following installers will be applied: []') 120 | and update_res.stdout_lines[1].endswith('RebootRequired.boolean=false') 121 | and update_res.stdout_lines[2].endswith('VIBsInstalled.string[] = ') 122 | and update_res.stdout_lines[3].endswith('VIBsRemoved.string[] = ')) 123 | when: update_test_res.changed 124 | 125 | - name: print update results 126 | debug: 127 | var: update_res.stdout_lines 128 | when: update_test_res.changed 129 | 130 | - name: check if reboot is required 131 | set_fact: 132 | reboot_required: "{{ update_test_res.stdout_lines[1].endswith('RebootRequired.boolean=true') }}" 133 | 134 | - name: determine if we will reboot host 135 | set_fact: 136 | will_reboot: "{{ reboot_required and (force_reboot or reboot_possible) }}" 137 | 138 | - name: print out reboot plans 139 | debug: 140 | msg: >- 141 | reboot: 142 | required: {{ reboot_required }} 143 | possible: {{ reboot_possible }} (force: {{ force_reboot }}) 144 | will do: {{ will_reboot }} 145 | 146 | # softer way: 147 | # esxcli system maintenanceMode set --enable true 148 | # esxcli system shutdown reboot --reason 'patch install' 149 | # ... 150 | # esxcli system maintenanceMode set --enable false 151 | - name: initiate host reboot 152 | shell: "/bin/reboot" 153 | when: will_reboot 154 | 155 | - name: wait for host to shut down 156 | local_action: wait_for 157 | args: 158 | host: "{{ ansible_fqdn }}" 159 | port: 22 160 | state: stopped 161 | delay: 20 162 | timeout: 180 163 | when: will_reboot 164 | 165 | - name: wait for host to boot 166 | local_action: wait_for 167 | args: 168 | host: "{{ ansible_fqdn }}" 169 | port: 22 170 | state: started 171 | delay: 30 172 | timeout: 300 173 | when: will_reboot 174 | 175 | # before that SSH is accessible but requesting password 176 | - name: give the host some time to recover 177 | pause: 178 | seconds: 30 179 | when: will_reboot 180 | 181 | - name: reset ssh connection to re-login after host is booted 182 | meta: reset_connection 183 | when: will_reboot 184 | 185 | - name: clean up patch bundle from temp location 186 | file: 187 | dest: "{{ temp_path }}/{{ bundle }}" 188 | state: absent 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Ansible has some great modules for VMware vCenter (especially in 2.5), but none for 3 | managing standalone ESXi hosts. There are many cases when full vCenter infrastructure 4 | is not required and web-based Host UI is quite enough for routine administrative tasks. 5 | 6 | Modules, roles and playbooks presented here allow to manage standalone ESXi hosts 7 | (although hosts under vCenter are ok too) with direct SSH connection, usually with 8 | transparent key-based authentication. 9 | 10 | # Contents of repository 11 | 12 | - role to configure ESXi host (`roles/hostconf_esxi`) 13 | - playbooks to deploy new VMs to ESXi host (in `vm_deploy/`) 14 | - by uploading (template) VM from some other host (`upload_clone`) 15 | - or by cloning local VM (`clone_local`) 16 | - modules used by role and deployment playbook 17 | - to gather VM facts from ESXi host (`esxi_vm_info`) 18 | - to manage autostart of VMs (`esxi_autostart`) 19 | - to install or update custom VIBs (`esxi_vib`) 20 | - some helper filter plugins to simplify working with ESXi shell commands output 21 | - `split`: split string into a list 22 | - `todict`: convert a list of records into a dictionary, using specified field as a key 23 | - example playbook to update ESXi host with offline bundle (`update_esxi.yaml`) 24 | - helper script to get vault pass from macOS keychain (`get_vault_pass.esxi.sh`) 25 | 26 | # `hostconf-esxi` role 27 | 28 | This role takes care of many aspects of standalone ESXi server configuration like 29 | 30 | - ESXi license key (if set) 31 | - host name, DNS servers 32 | - NTP servers, enable NTP client, set time 33 | - users 34 | - create missed, remove extra ones 35 | - assign random passwords to new users (and store in `creds/`) 36 | - make SSH keys persist across reboots 37 | - grant DCUI rights 38 | - portgroups 39 | - create missed, remove extra 40 | - assign specified tags 41 | - block BPDUs from guests 42 | - create vMotion interface (off by default, see `create_vmotion_iface` in role defaults) 43 | - datastores 44 | - partition specified devices if required 45 | - create missed datastores 46 | - rename empty ones with wrong names 47 | - autostart for specified VMs (optionally disabling it for all others) 48 | - logging to syslog server; lower `vpxa` and other noisy components logging level from 49 | default `verbose` to `info` 50 | - certificates for Host UI and SSL communication (if present) 51 | - install or update specified VIBs 52 | - [disable SLP](https://kb.vmware.com/s/article/76372), dangerous and mostly useless in smaller 53 | deployments (set `disable_slpd: true` in host vars to turn it off) 54 | 55 | Only requirement is correctly configured network (especially uplinks) and reachability 56 | over ssh with root password. ESXi must be reasonably recent (6.0+, although some 57 | newer versions of 5.5 have working python 2.7 too). 58 | 59 | ## General configuration 60 | - `ansible.cfg`: specify remote user, inventory path etc; specify vault pass method 61 | if using one for certificate private key encryption. 62 | - `group_vars/all.yaml`: specify global parameters like NTP and syslog servers there 63 | - `group_vars/.yaml`: set specific params for each `` in inventory 64 | - `host_vars/.yaml`: override global and group values with e.g. host-specific 65 | users list or datastore config 66 | - put public keys for users into `roles/hostconf-esxi/files/id_rsa.@.pub` 67 | for referencing them later in user list `host_vars` or `group_vars` 68 | 69 | ## Typical variables for `(group|host)_vars` 70 | - serial number to assign, usually set in global `group_vars/all.yaml`; does not get 71 | changed if not set 72 | 73 | esxi_serial: "XXXXX-XXXXX-XXXX-XXXXX-XXXXX" 74 | 75 | - general network environment, usually set in `group_vars/.yaml` 76 | 77 | dns_domain: "m0.maxidom.ru" 78 | 79 | name_servers: 80 | - 10.0.128.1 81 | - 10.0.128.2 82 | 83 | ntp_servers: 84 | - 10.1.131.1 85 | - 10.1.131.2 86 | 87 | # defaults: "log." + dns_domain 88 | # syslog_host: log.m0.maxidom.ru 89 | 90 | - user configuration: those users are created (if not present) and assigned random 91 | passwords (printed out and stored in `creds/..pass.out`), have ssh keys assigned to them (persistently) and restricted to specified hosts (plus global list 92 | in `permit_ssh_from`), are granted administrative rights and access to the console 93 | 94 | esxi_local_users: 95 | "": 96 | desc: """ 97 | pubkeys: 98 | - name: "" 99 | hosts: "1.2.3.4,some-host.com" 100 | 101 | users that are not in this list (except root) are removed from host, so be careful. 102 | - network configuration: portgroups list in `esxi_portgroups` are exhaustive, i.e. those 103 | and only those portgroups (with exactly matched tags) should be present oh host after 104 | playbook run (missed are created, wrong names are fixed, extra are removed if not used) 105 | 106 | esxi_portgroups: 107 | all-tagged: { tag: 4095 } 108 | adm-srv: { tag: 210 } 109 | srv-netinf: { tag: 131 } 110 | pvt-netinf: { tag: 199 } 111 | # could also specify vSwitch (default is vSwitch0) 112 | adm-stor: { tag: 21, vswitch: vSwitch1 } 113 | 114 | - datastore configuration: datastores would be created on those devices if missed and 115 | `create_datastores` is set; existent datastores would be renamed to match specified 116 | name if `rename_datastores` is set and they are empty 117 | 118 | local_datastores: 119 | "vmhba0:C0:T0:L1": "nest-test-sys" 120 | "vmhba0:C0:T0:L2": "nest-test-apps" 121 | 122 | - VIBs to install or update (like latest esx-ui host client fling) 123 | 124 | vib_list: 125 | - name: esx-ui 126 | url: "http://www-distr.m1.maxidom.ru/suse_distr/iso/esxui-signed-6360286.vib" 127 | 128 | - autostart configuration: listed VMs are added to esxi auto-start list, in specified order 129 | if order is present, else just randomly; if `autostart_only_listed` is set, only those VMs 130 | will be autostarted on host with extra VMs removed from autostart 131 | 132 | vms_to_autostart: 133 | eagle-m0: 134 | order: 1 135 | hawk-m0: 136 | order: 2 137 | falcon-u1: 138 | 139 | ## Host-specific configuration 140 | - add host into corresponding group in `inventory.esxi` 141 | - set custom certificate for host 142 | - put certificate into `files/.rui.crt`, 143 | - put key into `files/.key.vault` (and encrypt vault) 144 | - override any group vars in `host_vars/hostname.yaml` 145 | 146 | ## Initial host setup and later convergence runs 147 | 148 | For the initial config only the "root" user is available, so run playbook like this: 149 | 150 | ansible-playbook all.yaml -l new-host -u root -k --tags hostconf --diff 151 | 152 | After local users are configured (and ssh key auth is in place), just use `remote_user` 153 | from `ansible.cfg` and run it like 154 | 155 | ansible-playbook all.yaml -l host-or-group --tags hostconf --diff 156 | 157 | ## Notes 158 | - only one vSwitch (`vSwitch0`) is currently supported 159 | - password policy checks (introduced in 6.5) are turned off to allow for truly random 160 | passwords (those are sometimes miss one of the character classes). 161 | 162 | # VM deployment playbooks 163 | 164 | There are two playbooks in `vm_deploy/` subdir 165 | 166 | - first (`upload_clone`) is for copying template VM from source host to new target 167 | - second (`clone_local`) is for making custom clones of local template VM 168 | 169 | See playbook source and comments at the top for a list if parameters, some are 170 | mentioned below. 171 | 172 | ## Assumptions about environment 173 | 174 | - ansible 2.3+ (2.2 "replace" is not compatible with python 3 on ESXi) 175 | - local modules `netaddr` and `dnspython` 176 | - clone source must be powered off 177 | - for VM customization like setting IPs etc, [ovfconf](https://github.com/veksh/ovfconf) 178 | must be configured on clone source VM (to take advantage of passing OVF params to VM) 179 | 180 | ## `upload_clone` 181 | 182 | This playbooks is mostly used to upload initial "template" VM to target host (to be, 183 | in turn, template for further local cloning). Source of template VM is usually at 184 | another ESXi host, and there are 3 modes of copy: 185 | 186 | - direct "pull" SCP: destination host is SCP'ing VM files from source; authorization 187 | is key-based with agent forwarding, so both hosts must have current Ansible user 188 | configured and destination host must be in allowed hosts list for this user 189 | - direct "push" SCP: source host is SCP'ing VM files to destination, exactly as above 190 | (if e.g. firewall is more permissive in that direction) 191 | - slow copy via current hosts: download VM files from source to temp dir first (with 192 | Ansible "copy" module; rather fast if file is already staged there), then upload it 193 | to destination hosts (must have enough space in "tmp" for that, see `ansible-deploy.cfg` 194 | for tmp configuration) 195 | 196 | There are no options for customization there, only for src and dst params like datastore, 197 | and usual invocation looks like 198 | 199 | ansible-playbook upload_clone.yaml -l nest2-k1 \ 200 | -e 'src_vm_name=phoenix11-1-k1 src_vm_vol=nest1-sys src_vm_server=nest1-k1' \ 201 | -e 'dst_vm_name=phoenix11-2-k1' \ 202 | -e 'direct_scp=true' 203 | 204 | ## `clone_local` 205 | 206 | This playbook is used to produce new VM from local template source, optionally customize 207 | parameters like datastore, network and disks, and optionally power it on. Invocation 208 | to create new machine (with additional network card and disk) and power it on looks like 209 | 210 | ansible-playbook clone_local.yaml -l nest1-mf1 -e 'vm_name=files-mf1-vm \ 211 | vm_desc="samba file server" vm_net2=srv-smb vm_disk2=100G' \ 212 | -e 'do_power_on=true' 213 | 214 | To simplify cloning, it is better to 215 | 216 | - specify local clone source vm in ESXi host `host_vars` (as `src_vm_name`) 217 | - already have new machine's name in DNS (so IP is determined automatically) 218 | - have [ovfconf](https://github.com/veksh/ovfconf) configured in source (template) 219 | VM, as OVF is used to pass network config there (DHCP server would be ok too) 220 | 221 | # Modules 222 | 223 | Modules (`library/`) are documented with usual Ansible docs. They could be used 224 | stand-alone, like 225 | 226 | ansible -m esxi_vm_list -a 'get_power_state=true get_start_state=true' esxi-name 227 | 228 | to get a list of host VMs together with autostart state and current run state 229 | -------------------------------------------------------------------------------- /library/esxi_autostart.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | source ~/tmp/ansible/hacking/env-setup 4 | export PATH=$PATH:~/tmp/ansible/hacking/ 5 | test-module -m esxi_autostart.py -a 'name=eagle-m8 start=yes mock=yes' 6 | 7 | export ANSIBLE_CONFIG=~/works/sysadm/ansible-study/esxi-mgmt/ansible.esxi.cfg 8 | ansible -m esxi_autostart -a 'name=eagle-m8' nest1-m8 9 | ''' 10 | 11 | from ansible.module_utils.basic import AnsibleModule 12 | import re 13 | 14 | ANSIBLE_METADATA = {'status': ['preview'], 15 | 'supported_by': 'committer', 16 | 'version': '0.1'} 17 | 18 | DOCUMENTATION = ''' 19 | --- 20 | module: esx_autostart 21 | short_description: manages VM startup for stand-alone ESXi host 22 | version_added: "2.2" 23 | description: 24 | - 'This module manages VM startup on ESXi with ssh and C("vim-cmd").' 25 | - 'It allows to enable or disable autostart for named VM and optionally specify 26 | startup order.' 27 | options: 28 | name: 29 | description: 'Name of registered VM to manage' 30 | required: true 31 | aliases: ["vm"] 32 | enabled: 33 | description: 'Whether VM should be started at host starup' 34 | default: true 35 | aliases: ["autostart"] 36 | order: 37 | description: 'Relative startup order. If some VM already occupy this place, it is shifted 38 | down along with rest of VMs with higher startup order. List is always sequential 39 | and VMs are numbered from 1. By default, VMs are added at the end of startup list' 40 | required: false 41 | skip: 42 | description: 'Skip bad/not-yet-registered VM names without error (by default, it is error). 43 | Better check them in host facts if available :)' 44 | default: False 45 | state: 46 | description: 'Whether VM should be running now, default: do not change state' 47 | required: false 48 | choices: ["started", "stopped"] 49 | author: alex@maxidom.ru 50 | notes: 51 | - 'works w/o vcenter via C(ssh)' 52 | - 'Note that ESXi autostart manager API is rather buggy:' 53 | - 'There is no clear way to disable VM startup (module is setting start action to 54 | "PowerOff")' 55 | - 'Moving VM startup order around is OK as long as new order != end of startup 56 | list, in that case list is corrupted' 57 | - 'Setting order to wrong number (0, 999, etc) removes VM from command output 58 | but entry is still in C(/etc/vmware/hostd/vmAutoStart.xml); later actions could 59 | result in duplicate sequence numbers with unknown consequences' 60 | requirements: [] 61 | 62 | ''' 63 | 64 | 65 | EXAMPLES = ''' 66 | 67 | # make eagle-m8 autostart at first position 68 | - esxi_autostart: 69 | name: eagle-m8 70 | start: yes 71 | order: 1 72 | 73 | # disable phoenix11 autostart 74 | - esxi_autostart: 75 | name: phoenix11 76 | enabled: false 77 | 78 | ''' 79 | 80 | # either cat mock files (for test) or run actual cmds (for real) 81 | MOCK_DIR = '/Users/alex/works/sysadm/ansible-study/esxi-mgmt/experiments/mocks' 82 | COMMANDS = { 83 | 'real': { 84 | 'get_vmlist': 'vim-cmd vmsvc/getallvms', 85 | 'get_autoruns': 'vim-cmd hostsvc/autostartmanager/get_autostartseq', 86 | 'mod_start': 'vim-cmd hostsvc/autostartmanager/update_autostartentry ' + 87 | '{vm_id} "PowerOn" "10" "{order}" ' + 88 | '"guestShutdown" "systemDefault" "systemDefault"', 89 | # use '--' to mark end of options or else it will complain about -1 90 | 'disable_start': 'vim-cmd hostsvc/autostartmanager/update_autostartentry -- ' + 91 | '"{vm_id}" "PowerOff" "1" "-1" ' + 92 | '"guestShutdown" "systemDefault" "systemDefault"' 93 | }, 94 | 'mock': { 95 | 'get_vmlist': 'cat %s/getallvms.txt' % MOCK_DIR, 96 | 'get_autoruns': 'cat %s/get_autostartseq.txt' % MOCK_DIR, 97 | 'mod_start': 'echo "set {vm_id} to start at {order}"', 98 | 'disable_start': 'echo "disable {vm_id} startup at {order}"' 99 | }, 100 | } 101 | 102 | 103 | class VMStartMgr(object): 104 | """ manager for autostart entries """ 105 | 106 | def __init__(self, module): 107 | self.module = module 108 | self.params = self.module.params 109 | 110 | self.check_mode = module.check_mode 111 | self.mock = module.params['mock'] 112 | if self.mock: 113 | self.commands = COMMANDS['mock'] 114 | else: 115 | self.commands = COMMANDS['real'] 116 | self.vmname_to_id = self.load_vm_list() 117 | self.vm_start_info = self.load_startup_list() 118 | 119 | def load_vm_list(self): 120 | ''' construct map "vm_name -> vm_id" from file or program ''' 121 | vmlist = dict() 122 | ret, out, err = self.module.run_command(self.commands['get_vmlist']) 123 | if ret != 0: 124 | self.module.fail_json(msg="unable to get vm list", rc=ret, err=err) 125 | for line in out.split('\n'): 126 | if line.startswith('Vmid') or line == '': 127 | continue 128 | # multiline annotations are tricky 129 | if not re.match(r'^\d+ +\S+ +\[\S+\] \S+/\S+\.vmx', line): 130 | continue 131 | lfields = line.split() 132 | vmlist[lfields[1]] = int(lfields[0]) 133 | return vmlist 134 | 135 | def load_startup_list(self): 136 | ''' 137 | construct map "vm_id -> {autorun properties}" from command output 138 | currently we are interested in 139 | - "order": autostart order, int 1..N 140 | - "action": startAction from list; string, could be 141 | - "PowerOn": default 142 | - "PowerOff": one known way to disable autostart 143 | - DirectUI fling sets startOrder = -1 to disable autostart 144 | - lets use both to make sure :) 145 | ''' 146 | sinfo = dict() 147 | vm_id = 0 148 | # for line in file(STARTUP_FILE): 149 | # or subprocess.Popen + res.stdout.readlines 150 | # for line in os.popen("cat %s" % STARTUP_FILE).readlines(): 151 | ret, out, err = self.module.run_command(self.commands['get_autoruns']) 152 | if ret != 0: 153 | self.module.fail_json(msg="unable go get startup list", rc=ret, err=err) 154 | if out == '(vim.host.AutoStartManager.AutoPowerInfo) []': 155 | return sinfo 156 | for line in out.split('\n'): 157 | if line.lstrip().startswith(('(', '}', ']')) or line == '': 158 | continue 159 | (key, _, val) = line.strip("', \n").strip().split() 160 | if key == 'key': 161 | # key = 'vim.VirtualMachine:3', 162 | vm_id = int(val.split(":")[1]) 163 | sinfo[vm_id] = {} 164 | elif key == 'startOrder': 165 | sinfo[vm_id]['order'] = int(val) 166 | elif key == 'startAction': 167 | sinfo[vm_id]['action'] = val.strip('"') 168 | return sinfo 169 | 170 | def update_vm(self): 171 | ''' 172 | Perform actual autostart db update 173 | - adds vm to autostart manager db if not yet 174 | - changes order if specified 175 | - there is no clear way to remove VM, so disable it with startup action set to PowerOff 176 | ''' 177 | vm_name = self.params['name'] 178 | new_start = self.params['enabled'] 179 | new_order = self.params['order'] 180 | 181 | if vm_name not in self.vmname_to_id: 182 | if self.params['skip']: 183 | return (False, "VM %s not found, skipping" % vm_name, {}) 184 | else: 185 | self.module.fail_json(msg="no such vm here: %s" % vm_name, rc=-1) 186 | 187 | vm_id = self.vmname_to_id[vm_name] 188 | start_cmd = self.commands['mod_start'] 189 | disable_cmd = self.commands['disable_start'] 190 | 191 | # note module.check_mode and mock 192 | command = None 193 | changed = False 194 | ret_msg = 'all ok' 195 | ret_params = {'vm_id': vm_id} 196 | if not new_start: 197 | if vm_id in self.vm_start_info: 198 | old_order = self.vm_start_info[vm_id]['order'] 199 | old_action = self.vm_start_info[vm_id]['action'] 200 | if old_action != "PowerOff": 201 | changed = True 202 | ret_msg = "autostart disabled, moved to pos -1" 203 | command = disable_cmd.format(vm_id = vm_id) 204 | ret_params['old_action'] = old_action 205 | else: 206 | ret_msg = "already ok: autostart disabled" 207 | else: 208 | ret_msg = "already ok: not in autostart" 209 | else: 210 | if vm_id in self.vm_start_info: 211 | old_order = self.vm_start_info[vm_id]['order'] 212 | old_action = self.vm_start_info[vm_id]['action'] 213 | if old_action != "PowerOn" or old_order == -1: 214 | changed = True 215 | if new_order is None: 216 | new_order = len([v for v in self.vm_start_info.values() if v['order'] > 0]) + 1 217 | ret_msg = "autostart enabled at pos %d" % new_order 218 | ret_params['old_action'] = old_action 219 | ret_params['old_pos'] = old_order 220 | ret_params['new_pos'] = new_order 221 | command = start_cmd.format(vm_id = vm_id, order = new_order) 222 | if new_order is not None and new_order != old_order and not changed: 223 | changed = True 224 | command = start_cmd.format(vm_id = vm_id, order = new_order) 225 | ret_msg = "autostart enabled, moved from %d to %d" % (old_order, new_order) 226 | ret_params['old_pos'] = old_order 227 | ret_params['new_pos'] = new_order 228 | if not changed: 229 | ret_msg = "already ok: autostart enabled, pos %d" % old_order 230 | else: 231 | changed = True 232 | if new_order is None: 233 | new_order = len([v for v in self.vm_start_info.values() if v['order'] > 0]) + 1 234 | command = start_cmd.format(vm_id = vm_id, order = new_order) 235 | ret_msg = "autostart added at pos %d" % new_order 236 | ret_params['new_pos'] = new_order 237 | 238 | if command is not None: 239 | ret_params['command'] = command 240 | if not self.check_mode: 241 | ret, out, err = self.module.run_command(command) 242 | if ret != 0: 243 | self.module.fail_json(msg="unable to perform changes", 244 | cmd=command, rc=ret, err=err) 245 | ret_params['cmd_ret'] = ret 246 | ret_params['cmd_out'] = out 247 | ret_params['cmd_err'] = err 248 | return (changed, ret_msg, ret_params) 249 | 250 | 251 | def main(): 252 | ''' entry point, simple one for now 253 | run with test-module -m esxi_autostart.py -a "name=hren" 254 | ''' 255 | module = AnsibleModule( 256 | argument_spec = dict( 257 | name = dict(aliases=['vm'], required=True), 258 | enabled = dict(aliases=['autostart'], required=False, type='bool', default=True), 259 | order = dict(required=False, type='int'), 260 | state = dict(required=False, type='str', 261 | choices=["started", "stopped"]), 262 | mock = dict(required=False, type='bool', default=False), 263 | skip = dict(required=False, type='bool', default=False) 264 | ), 265 | supports_check_mode=True, 266 | required_one_of=[['enabled', 'state']], 267 | ) 268 | # module.debug('stated') 269 | mgr = VMStartMgr(module) 270 | changed, msg, params = mgr.update_vm() 271 | module.exit_json(changed=changed, msg=msg, **params) 272 | 273 | 274 | if __name__ == '__main__': 275 | main() 276 | -------------------------------------------------------------------------------- /vm_deploy/upload_clone.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # playbook to clone VM from one esxi host to another: will 3 | # - download VM files from src, upload to dst 4 | # - rename and clean up config parameters 5 | # - convert disk to thin 6 | # - prepare OVF parameters 7 | # - resgister VM on destination host 8 | 9 | # export ANSIBLE_CONFIG=/Users/alex/works/sysadm/ansible-study/esxi-mgmt/vm_deploy/ansible-deploy.esxi.cfg 10 | # ansible-playbook upload_clone.yaml -l nest-test -e 'dst_vm_name=phoenix11-test' -e 'dst_vm_ip=10.1.10.123' 11 | # or 12 | # ansible-playbook upload_clone.yaml -l nest-test -e @clone_vars.yaml 13 | # or with shorter names 14 | # ansible-playbook upload_clone.yaml -l nest1-m1 -e 'vm_name=dc1-m1-vm vm_desc="samba AD DC"' -e 'direct_scp=true push_scp=true' 15 | 16 | # deploying to Kazan: 1st phoenix, 40 minutes 17 | # ansible-playbook upload_clone.yaml -l nest1-k1 -e 'dst_vm_name=phoenix11-1-k1' 18 | # deploying to Kazan: clone from 1st to 2nd host, 4 minutes 19 | # ansible-playbook upload_clone.yaml -l nest2-k1 \ 20 | # -e 'src_vm_name=phoenix11-1-k1 src_vm_vol=nest1-sys src_vm_server=nest1-k1' \ 21 | # -e 'dst_vm_name=phoenix11-2-k1' \ 22 | # -e 'direct_scp=true' 23 | 24 | # parameters have sensible defaults; w/o args deployment would be 25 | # - from phoenix11 on cage7 (hard-coded, set in host_vars if different) 26 | # - to phoenix11 on :/vmfs/volume/-sys/ 27 | # - OVF hostname equals to VM name, IP config auto-guessed from DNS lookup 28 | # (assuming that host is already present in local DNS) 29 | # - set defaults in host_vars for src and dst vm params if fixed 30 | # - also all params are overrideable with cmdline vars, mb en masse like in 31 | # example "clone_vars.yaml": 32 | # 33 | # # source 34 | # src_vm_server: cage7 35 | # src_vm_name: phoenix11 36 | # src_vm_vol: infra.data 37 | # # destination 38 | # dst_vm_name: phoenix11-t 39 | # dst_vm_vol: nest-test-sys 40 | # dst_vm_ip: 10.1.10.123 41 | # dst_vm_gw: 10.1.10.1 42 | # gst_vm_net: adm-srv 43 | # 44 | # environment and operational notes 45 | # - full 10G phoenix deployment take about 10-15 minutes inside M1 46 | # - most files are copied to local host first and then transferred to dst 47 | # - remote host must have sufficient space in "remote_tmp" dir 48 | # - vmware default is /.ansible/tmp, about 50M 49 | # - ok for ansible.cfg: remote_tmp = $(df | awk 'NR==2 {print $6}')/tmp 50 | # - use -e 'direct_scp=true' to directly scp VMDK between src and dst hosts 51 | # - does not support check_mode etc 52 | # - requires agent forwarding between hosts 53 | # - much faster for remote deployments 54 | # - use -e 'direct_scp=true push_scp=true' to reverse direction of copy, i.e 55 | # to scp from source host to destination (sometimes firewalls beteen hosts are 56 | # less restrictive in that direction) 57 | # - ansible 2.2 "replace" is not compatible with python 3 58 | # - use 2.3 (it is ok) 59 | # - for 2.2 fix ./lib/python2.7/site-packages/ansible/modules/core/files/replace.py 60 | # - fixed module is included 61 | # - required local modules are "netaddr" and "dnspython" 62 | 63 | - hosts: all 64 | 65 | vars: 66 | src_vm: 67 | server: "{{ src_vm_server | default('cage7') }}" 68 | host: "{{ hostvars[src_vm_server | default('cage7')].ansible_host }}" 69 | name: "{{ src_vm_name | default('phoenix11') }}" 70 | # really redundant: could get it from vm 71 | path: "{{ '/vmfs/volumes/' + (src_vm_vol | default('infra.data')) }}" 72 | dst_vm: 73 | host: "{{ hostvars[inventory_hostname].ansible_host }}" 74 | # pass as '-e dst_vm_vol=ds_name'; default: 1st defined local datastore or hostname + "-sys" 75 | path: "{{ '/vmfs/volumes/' + (vm_vol | default(dst_vm_vol) | default(((local_datastores|d({'def': ansible_hostname + '-sys'})) | dictsort | first)[1])) }}" 76 | name: "{{ vm_name | default(dst_vm_name) | default(src_vm.name) }}" 77 | desc: "{{ vm_desc | default(dst_vm_desc) | default('clone of ' + src_vm.name) }}" 78 | net: "{{ vm_net | default(dst_vm_net) | default('adm-srv') }}" 79 | dst_ip_addr: "{{ dst_vm_ip | default(lookup('dig', dst_vm.name + '.' + ansible_dns.domain ))}}" 80 | dst_gateway: "{{ dst_vm_gw | default(dst_ip_addr | regex_replace('^(\\d+\\.\\d+\\.\\d+)\\..*$', '\\1.254')) }}" 81 | vm_conf: 82 | hostname: "{{ dst_vm.name }}" 83 | domain: "{{ ansible_dns.domain }}" 84 | ip: "{{ dst_ip_addr }}" 85 | gateway: "{{ dst_gateway }}" 86 | dns: "{{ ansible_dns.nameservers|join(',') }}" 87 | ntp: "ntp.{{ ansible_dns.domain }}" 88 | relay: "smtp.{{ ansible_dns.domain }}" 89 | syslog: "log.{{ ansible_dns.domain }}" 90 | # constants 91 | conf_to_copy: 92 | - vmx 93 | - nvram 94 | - vmsd 95 | - vmxf 96 | - vmdk 97 | # better use native copy (but REMOTE_TEMP is required) 98 | copy_with_scp: false 99 | # debug 100 | convert_to_thin: true 101 | direct_scp: false 102 | do_ovf_params: true 103 | do_register: true 104 | do_power_on: false 105 | # allow agent forwarding w/o ansible.cfg change 106 | ansible_ssh_extra_args: '-A' 107 | 108 | tasks: 109 | 110 | - name: check that play targets exactly one esxi host 111 | assert: 112 | that: 113 | - ansible_play_hosts|length == 1 114 | - ansible_os_family == "VMkernel" 115 | msg: "please target only one vmware host with this play" 116 | tags: test 117 | 118 | - name: check that target VM name is correct 119 | assert: 120 | that: 121 | - dst_ip_addr != 'NXDOMAIN' 122 | - vm_conf.ip | ipaddr 123 | msg: "please check that {{ dst_vm.name }}.{{ ansible_dns.domain }} is present in DNS" 124 | tags: test 125 | 126 | - name: check source dir 127 | stat: 128 | path: "{{ src_vm.path }}/{{ src_vm.name}}" 129 | delegate_to: "{{ src_vm.server }}" 130 | register: src_stat_res 131 | 132 | # mb: make sure that source VM is powered off 133 | - name: make sure source exist 134 | assert: 135 | that: src_stat_res.stat.isdir is defined and src_stat_res.stat.isdir 136 | 137 | - name: check destination volume 138 | stat: 139 | path: "{{ dst_vm.path }}" 140 | register: dst_stat_vol_res 141 | 142 | - name: make sure destination volume exist 143 | assert: 144 | that: dst_stat_vol_res.stat.exists 145 | 146 | - name: check destination dir 147 | stat: 148 | path: "{{ dst_vm.path }}/{{ dst_vm.name }}" 149 | register: dst_stat_res 150 | 151 | # mb: make sure that destination VM is not registered 152 | - name: make sure destination does not exist 153 | assert: 154 | that: not dst_stat_res.stat.exists 155 | 156 | - name: fetch configs from src to temp dir 157 | fetch: 158 | src: "{{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}.{{ item }}" 159 | dest: "{{ inventory_dir }}/tmp/{{ src_vm.server }}/{{ src_vm.name }}/{{ src_vm.name }}.{{ item }}" 160 | flat: true 161 | with_items: "{{ conf_to_copy }}" 162 | delegate_to: "{{ src_vm.server }}" 163 | 164 | - name: create destination dir 165 | file: 166 | path: "{{ dst_vm.path }}/{{ dst_vm.name }}" 167 | state: directory 168 | 169 | - name: upload configs to dest dir 170 | copy: 171 | src: "{{ inventory_dir }}/tmp/{{ src_vm.server }}/{{ src_vm.name }}/{{ src_vm.name }}.{{ item }}" 172 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.{{ item }}" 173 | with_items: "{{ conf_to_copy }}" 174 | 175 | # does not work with ansible 2.2.3.0 on 6.5 (python 3.5.1): broken re 176 | # error is "TypeError: cannot use a string pattern on a bytes-like object" 177 | # fix at https://github.com/ansible/ansible/pull/19188/files (dec 11 2016) 178 | # 2.3.0.0 is ok: use it or patch 2.2 179 | - name: replace vm name in vmx config and vmdk 180 | replace: 181 | regexp: '"{{ src_vm.name }}([^"]*)"' 182 | replace: '"{{ dst_vm.name }}\1"' 183 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.{{ item }}" 184 | with_items: 185 | - vmx 186 | - vmdk 187 | 188 | - name: replace vm name in vmxf config 189 | replace: 190 | regexp: '>{{ src_vm.name }}\.vmx<' 191 | replace: '>{{ dst_vm.name }}.vmx<' 192 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmxf" 193 | 194 | - name: clean vmx config from volatile params 195 | lineinfile: 196 | regexp: "^{{ item }} = " 197 | state: absent 198 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 199 | with_items: 200 | - ethernet0.generatedAddress 201 | - uuid.location 202 | - uuid.bios 203 | - vc.uuid 204 | - sched.swap.derivedName 205 | 206 | - name: customize vmx config params 207 | lineinfile: 208 | regexp: '^{{ item.key }} = .*$' 209 | line: '{{ item.key }} = "{{ item.value }}"' 210 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 211 | with_dict: 212 | "ethernet0.addressType": "generated" 213 | "annotation": "{{ dst_vm.desc }}" 214 | "ethernet0.networkName": "{{ dst_vm.net }}" 215 | 216 | # better not back it up :) 217 | - name: fetch vm disk from src to temp dir 218 | fetch: 219 | src: "{{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}-flat.vmdk" 220 | dest: "{{ inventory_dir }}/tmp/{{ src_vm.server }}/{{ src_vm.name }}/{{ src_vm.name }}-flat.vmdk" 221 | flat: true 222 | delegate_to: "{{ src_vm.server }}" 223 | when: not direct_scp 224 | 225 | # set remote_tmp to large dir on same FS (to rename) 226 | # or export ANSIBLE_REMOTE_TEMP=/vmfs/volumes/nest-test-sys/tmp 227 | # or set in ansible.cfg remote_tmp = $(df | awk 'NR==2 {print $6}')/tmp 228 | # or copy manaully with scp (see below) 229 | # 10G: 1:31 if already ok (just checksumming), 7:12 if not 230 | - name: upload vm disk to dest dir 231 | copy: 232 | src: "{{ inventory_dir }}/tmp/{{ src_vm.server }}/{{ src_vm.name }}/{{ src_vm.name }}-flat.vmdk" 233 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}-flat.vmdk" 234 | when: not direct_scp 235 | 236 | # copy manually; does not support check_mode and stuff, but does not require TMP :) 237 | # relies on ssh agent forwarding 238 | # set it up in ansible.cfg or set "ansible_ssh_extra_args: '-A'" in vars 239 | - name: directly scp vm disk, pull dst <- src (if not using upload above) 240 | shell: > 241 | scp -o StrictHostKeyChecking=no \ 242 | {{ansible_user_id}}@{{src_vm.host}}:{{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}-flat.vmdk \ 243 | {{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}-flat.vmdk 244 | when: direct_scp and not push_scp 245 | 246 | # same but in reverse direction; sometimes firewall is more permissive that way 247 | - name: directly scp vm disk, push src -> dst (if not using upload above) 248 | shell: > 249 | scp -o StrictHostKeyChecking=no {{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}-flat.vmdk \ 250 | {{ansible_user_id}}@{{dst_vm.host}}:{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}-flat.vmdk 251 | delegate_to: "{{ src_vm.server }}" 252 | when: direct_scp and push_scp 253 | 254 | # convert by punching holes 255 | - name: convert VM disk to thin 256 | shell: 257 | vmkfstools -K {{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmdk 258 | when: convert_to_thin 259 | 260 | - name: add OVF params to VM config 261 | lineinfile: 262 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 263 | line: 'guestinfo.ovfEnv = "|0A|0A|0A|0A|0A|0A|0A|0A"' 264 | regexp: '^guestinfo.ovfEnv ' 265 | when: do_ovf_params 266 | 267 | # unregister: vim-cmd vmsvc/unregister 268 | - name: register VM 269 | shell: "vim-cmd solo/registervm {{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx {{ dst_vm.name }}" 270 | register: vm_register_res 271 | when: do_register 272 | 273 | - name: power on newly registered VM 274 | shell: "vim-cmd vmsvc/power.on {{ vm_register_res.stdout }}" 275 | when: do_power_on 276 | -------------------------------------------------------------------------------- /vm_deploy/clone_local.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # playbook to clone VM from local template vm on same host 3 | # - clone VM disk and configs 4 | # - rename and clean up config parameters 5 | # - prepare OVF parameters 6 | # - optionally, add 2nd disk and network card 7 | # - register and optionally power on new VM 8 | 9 | # export ANSIBLE_CONFIG=/Users/alex/works/sysadm/ansible-study/esxi-mgmt/vm_deploy/ansible-deploy.esxi.cfg 10 | # ansible-playbook clone_local.yaml -l nest-test -e 'dst_vm_name=mynewvm-with-ok-dns' 11 | # or 12 | # ansible-playbook clone_local.yaml -l nest-test -e @clone_vars.yaml -e 'dst_vm_desc="new copy"' 13 | # or fine-tuned like 14 | # ansible-playbook clone_local.yaml -l nest-test -e 'src_vm_name=phoenix11-test' 15 | # -e 'dst_vm_name=pdcnt-vm' -e 'dst_vm_desc="test samba3 pdc"' -e 'dst_vm_net2=servers-tst' 16 | # -e 'do_power_on=true' 17 | # ansible-playbook clone_local.yaml -l nest-test -e 'src_vm_name=phoenix11-test' -e 'dst_vm_name=member1-ad3-vm' 18 | # -e 'dst_vm_desc="test samba4 server (from playbook)"' -e 'dst_vm_net2=servers-tst' -e 'do_power_on=true' 19 | # ansible-playbook clone_local.yaml -l nest1-mf1 -e 'dst_vm_name=dc1-mf1-vm dst_vm_desc="samba AD domain controller" dst_vm_net2=srv-smb do_power_on=true' 20 | # or with shorter names, assuming that src_vm_name is set in host config 21 | # ansible-playbook clone_local.yaml -l nest-test -e 'vm_name=clone-vm vm_desc="test server" vm_net2=servers-tst vm_disk2=10G,nest-test-apps vm_cpus=2 vm_mem=4096' 22 | # ansible-playbook clone_local.yaml -l nest1-mf1 -e 'vm_name=dc1-mf1-vm vm_desc="samba AD domain controller" vm_net2=srv-smb power_on=true' 23 | # ansible-playbook clone_local.yaml -l nest1-mf1 -e 'vm_name=files-mf1-vm vm_desc="samba file server" vm_net2=srv-smb vm_disk2=100G' -e 'do_power_on=true' 24 | 25 | 26 | # only required arg is dst_vm_name, defaults are: 27 | # - clone from "default_src_vm_name" (host_vars) on -sys to same datastore, thin by default 28 | # params: src_vm_name + src_vm_vol; dst_vm_vol 29 | # - network plugged to same portgroup (param: dst_vm_net), OVF hostname equals to VM name, 30 | # IP config auto-guessed from DNS lookup (so new host must be in DNS already) 31 | # params: dst_vm_net, dst_vm_ip 32 | # - vm will be registered and not powered on 33 | # params: do_register, do_power_on 34 | # - it is also possible to add 2nd net card and 2nd disk to clone (default: none) 35 | # - net: -e 'dst_vm_net2=servers-tst' 36 | # - disk: -e 'dst_vm_disk2=1G,nest-test-apps' (or just 'dst_vm_disk2=10G' for same datastore) 37 | 38 | # all params are overrideable with cmdline vars; example "clone_vars.yaml": 39 | # 40 | # # source 41 | # src_vm_name: phoenix11 42 | # src_vm_vol: infra.data 43 | # # destination 44 | # dst_vm_name: newvm 45 | # dst_vm_desc: new test vm 46 | # dst_vm_vol: nest-test-apps 47 | # dst_vm_ip: 10.1.10.123 48 | # dst_vm_gw: 10.1.10.1 49 | # dst_vm_net: adm-srv 50 | # # optional additional hardware 51 | # dst_vm_net2: "srv-smb" 52 | # dst_vm_disk2: "10G,nest-test-apps" 53 | # dst_vm_cpus: 2 54 | # dst_vm_mem: 16384 55 | 56 | # environment and operational notes 57 | # - full 10G phoenix clone take about 2-3 minutes inside M1 58 | # - ansible 2.2 "replace" is not compatible with python 3 59 | # - use 2.3 (it is ok) 60 | # - for 2.2 fix ./lib/python2.7/site-packages/ansible/modules/core/files/replace.py 61 | # - fixed module is included 62 | # - required local modules are "netaddr" and "dnspython" 63 | 64 | - hosts: all 65 | 66 | vars: 67 | # default for source and dest: 1st host volume if defined, else hostname + '-sys' 68 | default_vol: "{{ src_vm_vol | default(((local_datastores|d({'def': ansible_hostname + '-sys'})) | dictsort | first)[1]) }}" 69 | # better have it defined, or conditions would be extremely complex 70 | dsk2: "{{ vm_disk2 | default(dst_vm_disk2) | default ('') }}" 71 | src_vm: 72 | name: "{{ src_vm_name | default('phoenix11') }}" 73 | # really redundant: could get it from vm 74 | path: "{{ '/vmfs/volumes/' + (src_vm_vol | default(default_vol)) }}" 75 | dst_vm: 76 | name: "{{ vm_name | default(dst_vm_name) }}" 77 | path: "{{ '/vmfs/volumes/' + (vm_vol | default(dst_vm_vol) | default(default_vol)) }}" 78 | desc: "{{ vm_desc | default(dst_vm_desc) | default('clone of ' + src_vm.name) }}" 79 | net: "{{ vm_net | default(dst_vm_net) | default('') }}" 80 | net2: "{{ vm_net2 | default(dst_vm_net2) | default('') }}" 81 | cpus: "{{ vm_cpus | default(dst_vm_cpus) | default('') }}" 82 | mem: "{{ vm_mem | default(dst_vm_mem) | default('') }}" 83 | # full format: "10G,nest-test-apps"; short: just "10G" (same datastore as VM) 84 | disk2_size: "{{ dsk2 | regex_replace('^([0-9]+[KGMkgm]).*$', '\\1') if dsk2 != '' else '' }}" 85 | # default: same as VM if not in arg 86 | disk2_path: "{{ '/vmfs/volumes/' + 87 | ((',' in dsk2) | ternary ( 88 | dsk2 | regex_replace('^[0-9]+[KGMkgm],?', ''), 89 | vm_vol | default(dst_vm_vol) | default(default_vol)) 90 | ) if dsk2 != '' else '' }}" 91 | dst_ip_addr: "{{ vm_ip | default(dst_vm_ip) | default(lookup('dig', dst_vm.name + '.' + ansible_dns.domain ))}}" 92 | dst_gateway: "{{ vm_gw | default(dst_vm_gw) | default(dst_ip_addr | regex_replace('^(\\d+\\.\\d+\\.\\d+)\\..*$', '\\1.254')) }}" 93 | vm_conf: 94 | hostname: "{{ dst_vm.name }}" 95 | domain: "{{ ansible_dns.domain }}" 96 | ip: "{{ dst_ip_addr }}" 97 | gateway: "{{ dst_gateway }}" 98 | dns: "{{ ansible_dns.nameservers|join(',') }}" 99 | ntp: "ntp.{{ ansible_dns.domain }}" 100 | relay: "smtp.{{ ansible_dns.domain }}" 101 | syslog: "log.{{ ansible_dns.domain }}" 102 | # constants 103 | conf_to_copy: 104 | - vmx 105 | - nvram 106 | - vmsd 107 | pci_slot_addl_card: 224 108 | # optional parts 109 | convert_to_thin: true 110 | do_ovf_params: true 111 | do_register: true 112 | do_power_on: false 113 | 114 | tasks: 115 | 116 | - debug: var=src_vm 117 | tags: test 118 | - debug: var=dst_vm 119 | tags: test 120 | - debug: var=vm_conf 121 | tags: test 122 | 123 | # - meta: end_play 124 | 125 | - name: check that play targets exactly one esxi host 126 | assert: 127 | that: 128 | - ansible_play_hosts|length == 1 129 | - ansible_os_family == "VMkernel" 130 | msg: "please target only one vmware host with this play" 131 | 132 | - name: check that target VM name is correct 133 | assert: 134 | that: 135 | - dst_vm.name is defined 136 | - dst_ip_addr != 'NXDOMAIN' 137 | - vm_conf.ip | ipaddr 138 | msg: "please check that {{ dst_vm.name }}.{{ ansible_dns.domain }} is present in DNS" 139 | 140 | - name: check rest of params for consistency 141 | assert: 142 | that: 143 | - dst_vm.disk2_path == '' or dst_vm.disk2_size != '' 144 | 145 | # mb: make sure that source VM is powered off 146 | - name: make sure source dir exist 147 | stat: 148 | path: "{{ src_vm.path }}/{{ src_vm.name}}" 149 | register: src_stat_res 150 | failed_when: not (src_stat_res.stat.isdir is defined and src_stat_res.stat.isdir) 151 | 152 | - name: make sure destination volume exists 153 | stat: 154 | path: "{{ dst_vm.path }}" 155 | register: dst_stat_vol_res 156 | failed_when: not dst_stat_vol_res.stat.exists 157 | 158 | - name: check that VM folder on destination dir does not exist yet 159 | stat: 160 | path: "{{ dst_vm.path }}/{{ dst_vm.name }}" 161 | register: dst_stat_res 162 | failed_when: dst_stat_res.stat.exists 163 | 164 | - name: check that destination volume for 2nd disk exists 165 | stat: 166 | path: "{{ dst_vm.disk2_path }}" 167 | register: dst_stat_vol2_res 168 | failed_when: not dst_stat_vol2_res.stat.exists 169 | when: dst_vm.disk2_path != '' 170 | 171 | - name: create destination dir 172 | file: 173 | path: "{{ dst_vm.path }}/{{ dst_vm.name }}" 174 | state: directory 175 | 176 | - name: copy configs to dest dir 177 | copy: 178 | src: "{{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}.{{ item }}" 179 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.{{ item }}" 180 | remote_src: true 181 | with_items: "{{ conf_to_copy }}" 182 | 183 | # does not work with ansible 2.2.3.0 on 6.5 (python 3.5.1): broken re 184 | # error is "TypeError: cannot use a string pattern on a bytes-like object" 185 | # fix at https://github.com/ansible/ansible/pull/19188/files (dec 11 2016) 186 | # 2.3.0.0 is ok: use it or patch 2.2 187 | - name: replace vm name in vmx config 188 | replace: 189 | regexp: '"{{ src_vm.name }}([^"]*)"' 190 | replace: '"{{ dst_vm.name }}\1"' 191 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.{{ item }}" 192 | with_items: 193 | - vmx 194 | 195 | - name: clean vmx config from volatile params 196 | lineinfile: 197 | regexp: "^{{ item }} = " 198 | state: absent 199 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 200 | with_items: 201 | - ethernet0.generatedAddress 202 | - uuid.location 203 | - uuid.bios 204 | - vc.uuid 205 | - sched.swap.derivedName 206 | 207 | # empty means "unchanged" 208 | - name: customize vmx config params 209 | lineinfile: 210 | regexp: '^{{ item.key }} = .*$' 211 | line: '{{ item.key }} = "{{ item.value }}"' 212 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 213 | when: item.value is defined and item.value != '' 214 | with_dict: 215 | "ethernet0.addressType": "generated" 216 | "annotation": "{{ dst_vm.desc }}" 217 | "ethernet0.networkName": "{{ dst_vm.net }}" 218 | "numvcpus": "{{ dst_vm.cpus }}" 219 | "memSize": "{{ dst_vm.mem }}" 220 | 221 | # 43s for local thin copy 222 | - name: clone VM disk 223 | shell: 224 | vmkfstools -i {{ src_vm.path }}/{{ src_vm.name }}/{{ src_vm.name }}.vmdk 225 | {{ convert_to_thin | ternary ("-d thin", "") }} 226 | {{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmdk 227 | 228 | - name: add OVF params to VM config 229 | lineinfile: 230 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 231 | line: 'guestinfo.ovfEnv = "|0A|0A|0A|0A|0A|0A|0A|0A"' 232 | regexp: '^guestinfo.ovfEnv ' 233 | when: do_ovf_params 234 | 235 | # pci slot number: a bit tricky 236 | # - for phoenix children 1st is usually 192, 2nd 224 237 | # - for centos 1st is 160 (and 1st card is "ens160"), 2nd is 192 238 | # so lets make 2nd card 224 239 | - name: add 2nd network card 240 | blockinfile: 241 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 242 | block: | 243 | ethernet1.present = "true" 244 | ethernet1.pciSlotNumber = "{{ pci_slot_addl_card }}" 245 | ethernet1.virtualDev = "vmxnet3" 246 | ethernet1.networkName = "{{ dst_vm.net2 }}" 247 | ethernet1.addressType = "generated" 248 | marker: "# {mark} 2nd network card" 249 | when: dst_vm.net2 is defined and dst_vm.net2 != '' 250 | 251 | - name: create directory for 2nd disk 252 | file: 253 | path: "{{ dst_vm.disk2_path }}/{{ dst_vm.name }}" 254 | state: directory 255 | when: dst_vm.disk2_path != '' 256 | 257 | - name: create 2nd disk 258 | shell: 259 | vmkfstools -c {{ dst_vm.disk2_size }} 260 | {{ convert_to_thin | ternary ("-d thin", "") }} 261 | {{ dst_vm.disk2_path }}/{{ dst_vm.name }}/{{ dst_vm.name }}-disk1.vmdk 262 | when: dst_vm.disk2_path != '' 263 | 264 | - name: add 2nd disk to config 265 | blockinfile: 266 | dest: "{{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx" 267 | block: | 268 | scsi0:1.deviceType = "scsi-hardDisk" 269 | scsi0:1.fileName = "{{ dst_vm.disk2_path }}/{{ dst_vm.name }}/{{ dst_vm.name }}-disk1.vmdk" 270 | scsi0:1.present = "TRUE" 271 | scsi0:1.redo = "" 272 | marker: "# {mark} 2nd disk" 273 | when: dst_vm.disk2_path != '' 274 | 275 | # unregister: vim-cmd vmsvc/unregister 276 | - name: register VM 277 | shell: "vim-cmd solo/registervm {{ dst_vm.path }}/{{ dst_vm.name }}/{{ dst_vm.name }}.vmx {{dst_vm.name }}" 278 | register: vm_register_res 279 | when: do_register 280 | 281 | - name: power on newly registered VM 282 | shell: "vim-cmd vmsvc/power.on {{ vm_register_res.stdout }}" 283 | when: do_power_on 284 | --------------------------------------------------------------------------------