├── tests ├── .gitignore ├── roles │ └── yaegashi.blockinfile ├── expected │ ├── test-follow │ │ ├── link2.txt │ │ ├── real0.txt │ │ ├── real1.txt │ │ ├── link0.txt │ │ ├── link1.txt │ │ └── real2.txt │ ├── test-basic │ │ ├── remove.txt │ │ ├── insert.txt │ │ ├── update.txt │ │ ├── create.txt │ │ └── marker.txt │ ├── test-state │ │ ├── remove.txt │ │ ├── remove-absent.txt │ │ ├── remove-present.txt │ │ ├── insert.txt │ │ └── insert-present.txt │ ├── test-block │ │ ├── block.txt │ │ └── content.txt │ ├── test-insertab │ │ ├── insertafter.txt │ │ ├── insertbefore.txt │ │ ├── insertbof.txt │ │ ├── inserteof.txt │ │ ├── updateafter.txt │ │ ├── updatebefore.txt │ │ └── insertunmatched.txt │ └── test-replace │ │ └── index.html ├── fixtures │ ├── test-follow │ │ ├── link0.txt │ │ ├── link1.txt │ │ ├── link2.txt │ │ ├── real0.txt │ │ ├── real1.txt │ │ └── real2.txt │ ├── test-block │ │ ├── block.txt │ │ └── content.txt │ ├── test-basic │ │ ├── marker.txt │ │ ├── insert.txt │ │ ├── remove.txt │ │ └── update.txt │ ├── test-state │ │ ├── insert.txt │ │ ├── insert-present.txt │ │ ├── remove.txt │ │ ├── remove-absent.txt │ │ └── remove-present.txt │ ├── test-insertab │ │ ├── insertafter.txt │ │ ├── insertbof.txt │ │ ├── inserteof.txt │ │ ├── insertbefore.txt │ │ ├── insertunmatched.txt │ │ ├── updateafter.txt │ │ └── updatebefore.txt │ └── test-replace │ │ └── index.html ├── hosts ├── test-block.yml ├── test-replace.yml ├── test-follow.yml ├── test-state.yml ├── test-basic.yml ├── test-insertab.yml └── run.sh ├── meta └── main.yml ├── CONTRIBUTING.md ├── README.md └── library └── blockinfile.py /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /testing 2 | -------------------------------------------------------------------------------- /tests/roles/yaegashi.blockinfile: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /tests/expected/test-follow/link2.txt: -------------------------------------------------------------------------------- 1 | real2.txt -------------------------------------------------------------------------------- /tests/fixtures/test-follow/link0.txt: -------------------------------------------------------------------------------- 1 | real0.txt -------------------------------------------------------------------------------- /tests/fixtures/test-follow/link1.txt: -------------------------------------------------------------------------------- 1 | real1.txt -------------------------------------------------------------------------------- /tests/fixtures/test-follow/link2.txt: -------------------------------------------------------------------------------- 1 | real2.txt -------------------------------------------------------------------------------- /tests/fixtures/test-block/block.txt: -------------------------------------------------------------------------------- 1 | AAA BBB CCC 2 | -------------------------------------------------------------------------------- /tests/fixtures/test-block/content.txt: -------------------------------------------------------------------------------- 1 | AAA BBB CCC 2 | -------------------------------------------------------------------------------- /tests/hosts: -------------------------------------------------------------------------------- 1 | localhost ansible_connection=local 2 | -------------------------------------------------------------------------------- /tests/fixtures/test-basic/marker.txt: -------------------------------------------------------------------------------- 1 |
!!! @@@ ###
2 | -------------------------------------------------------------------------------- /tests/expected/test-block/block.txt: -------------------------------------------------------------------------------- 1 | AAA BBB CCC 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | aaa bbb ccc 4 | # END ANSIBLE MANAGED BLOCK 5 | -------------------------------------------------------------------------------- /tests/expected/test-block/content.txt: -------------------------------------------------------------------------------- 1 | AAA BBB CCC 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | aaa bbb ccc 4 | # END ANSIBLE MANAGED BLOCK 5 | -------------------------------------------------------------------------------- /tests/expected/test-state/insert.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | # END ANSIBLE MANAGED BLOCK 6 | -------------------------------------------------------------------------------- /tests/expected/test-state/insert-present.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | # END ANSIBLE MANAGED BLOCK 6 | -------------------------------------------------------------------------------- /tests/expected/test-basic/insert.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | AAA BBB CCC 6 | # END ANSIBLE MANAGED BLOCK 7 | -------------------------------------------------------------------------------- /tests/expected/test-basic/update.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | aaa bbb ccc 4 | AAA BBB CCC 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/expected/test-follow/link0.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | AAA BBB CCC 6 | # END ANSIBLE MANAGED BLOCK 7 | -------------------------------------------------------------------------------- /tests/expected/test-follow/link1.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | AAA BBB CCC 6 | # END ANSIBLE MANAGED BLOCK 7 | -------------------------------------------------------------------------------- /tests/expected/test-follow/real2.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | !!! @@@ ### 3 | # BEGIN ANSIBLE MANAGED BLOCK 4 | aaa bbb ccc 5 | AAA BBB CCC 6 | # END ANSIBLE MANAGED BLOCK 7 | -------------------------------------------------------------------------------- /tests/fixtures/test-basic/remove.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | xxx yyy zzz 4 | XXX YYY ZZZ 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/fixtures/test-basic/update.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | xxx yyy zzz 4 | XXX YYY ZZZ 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/fixtures/test-state/remove.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | xxx yyy zzz 4 | XXX YYY ZZZ 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/fixtures/test-state/remove-absent.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | xxx yyy zzz 4 | XXX YYY ZZZ 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/fixtures/test-state/remove-present.txt: -------------------------------------------------------------------------------- 1 | 111 222 333 2 | # BEGIN ANSIBLE MANAGED BLOCK 3 | xxx yyy zzz 4 | XXX YYY ZZZ 5 | # END ANSIBLE MANAGED BLOCK 6 | !!! @@@ ### 7 | -------------------------------------------------------------------------------- /tests/expected/test-basic/create.txt: -------------------------------------------------------------------------------- 1 | # BEGIN ANSIBLE MANAGED BLOCK 2 | iface eth0 inet static 3 | address 192.168.0.1 4 | netmask 255.255.255.0 5 | # END ANSIBLE MANAGED BLOCK 6 | -------------------------------------------------------------------------------- /tests/expected/test-basic/marker.txt: -------------------------------------------------------------------------------- 1 |!!! @@@ ###
7 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: YAEGASHI Takeshi 4 | description: Contains blockinfile module 5 | to insert/update/remove a text block surrounded by marker lines 6 | license: GPLv3+ 7 | min_ansible_version: 1.2 8 | categories: 9 | - system 10 | dependencies: [] 11 | -------------------------------------------------------------------------------- /tests/test-block.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | vars: 3 | - testing_dir: "{{playbook_dir}}/testing" 4 | roles: 5 | - yaegashi.blockinfile 6 | tasks: 7 | - name: insert a block using block 8 | blockinfile: 9 | dest: "{{testing_dir}}/block.txt" 10 | backup: yes 11 | block: aaa bbb ccc 12 | - name: insert a block using block 13 | blockinfile: 14 | dest: "{{testing_dir}}/content.txt" 15 | backup: yes 16 | content: aaa bbb ccc 17 | -------------------------------------------------------------------------------- /tests/test-replace.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | vars: 3 | - testing_dir: "{{playbook_dir}}/testing" 4 | roles: 5 | - yaegashi.blockinfile 6 | tasks: 7 | - name: insert a newline after 8 | replace: 9 | dest: "{{testing_dir}}/index.html" 10 | backup: yes 11 | regexp: (?i)()(?=.) 12 | replace: \1\n 13 | - name: insert a block after 14 | blockinfile: 15 | dest: "{{testing_dir}}/index.html" 16 | backup: yes 17 | marker: "" 18 | content: | 19 || parameter | 63 |required | 64 |default | 65 |choices | 66 |comments | 67 |
|---|---|---|---|---|
| backup |
70 | no | 71 |no | 72 |
|
73 | Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. |
| block |
76 | no | 77 |78 | | The text to insert inside the marker lines. If it's missing or an empty string, the block will be removed as if
80 | state were specified to absent.aliases: content | |
| create |
83 | no | 84 |no | 85 |
|
86 | Create a new file if it doesn't exist. |
| dest |
89 | yes | 90 |91 | | The file to modify.
93 | aliases: name, destfile | |
| follow (added in 1.8) |
96 | no | 97 |no | 98 |
|
99 | This flag indicates that filesystem links, if they exist, should be followed. |
| group |
102 | no | 103 |104 | | name of the group that should own the file/directory, as would be fed to chown | |
| insertafter |
108 | no | 109 |EOF | 110 |
|
111 | If specified, the block will be inserted after the last match of specified regular expression. A special value is available; EOF for inserting the block at the end of the file. If specified regular expresion has no matches, EOF will be used instead. |
| insertbefore |
114 | no | 115 |116 | |
|
117 | If specified, the block will be inserted before the last match of specified regular expression. A special value is available; BOF for inserting the block at the beginning of the file. If specified regular expresion has no matches, the block will be inserted at the end of the file. |
| marker |
120 | no | 121 |# {mark} ANSIBLE MANAGED BLOCK | 122 |The marker line template. "{mark}" will be replaced with "BEGIN" or "END". | |
| mode |
126 | no | 127 |128 | | mode the file or directory should be. For those used to /usr/bin/chmod remember that modes are actually octal numbers (like 0644). Leaving off the leading zero will likely have unexpected results. As of version 1.8, the mode may be specified as a symbolic mode (for example, u+rwx or u=rw,g=r,o=r). | |
| owner |
132 | no | 133 |134 | | name of the user that should own the file/directory, as would be fed to chown | |
| selevel |
138 | no | 139 |s0 | 140 |level part of the SELinux file context. This is the MLS/MCS attribute, sometimes known as the range. _default feature works as for seuser. | |
| serole |
144 | no | 145 |146 | | role part of SELinux file context, _default feature works as for seuser. | |
| setype |
150 | no | 151 |152 | | type part of SELinux file context, _default feature works as for seuser. | |
| seuser |
156 | no | 157 |158 | | user part of SELinux file context. Will default to system policy, if applicable. If set to _default, it will use the user portion of the policy if available | |
| state |
162 | no | 163 |present | 164 |
|
165 | Whether the block should be there or not. |
| validate |
168 | no | 169 |None | 170 |The validation command to run before copying into place. The path to the file to validate is passed in via '%s' which must be present as in the example below. The command is passed securely so shell features like expansion and pipes won't work. |
Last updated on {{ansible_date_time.iso8601}}
205 | ``` 206 | 207 | ```yaml 208 | - name: remove HTML as well as surrounding markers 209 | blockinfile: 210 | dest: /var/www/html/index.html 211 | marker: "" 212 | content: "" 213 | ``` 214 | 215 | ## Requirements 216 | 217 | None. 218 | 219 | ## Role Variables 220 | 221 | None. 222 | 223 | ## Dependencies 224 | 225 | None. 226 | 227 | ## Example Playbook 228 | 229 | Complete playbook 230 | that makes SSH password authentication for specific user prohibited, 231 | then restarts sshd if needed. 232 | 233 | ```yaml 234 | --- 235 | - hosts: all 236 | remote_user: ansible-agent 237 | sudo: yes 238 | roles: 239 | - yaegashi.blockinfile 240 | tasks: 241 | - name: Prohibit SSH password authentication for $SUDO_USER 242 | blockinfile: 243 | dest: /etc/ssh/sshd_config 244 | backup: yes 245 | content: | 246 | Match User {{ansible_env.SUDO_USER}} 247 | PasswordAuthentication no 248 | notify: Restart sshd 249 | handlers: 250 | - name: Restart sshd 251 | service 252 | name: ssh 253 | state: restarted 254 | ``` 255 | 256 | ## License 257 | 258 | GPLv3+ 259 | 260 | ## Author Information 261 | 262 | [YAEGASHI Takeshi](https://github.com/yaegashi) 263 | -------------------------------------------------------------------------------- /library/blockinfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2014, 2015 YAEGASHI TakeshiLast updated on {{ansible_date_time.iso8601}}
126 | 127 | - name: remove HTML as well as surrounding markers 128 | blockinfile: 129 | dest: /var/www/html/index.html 130 | marker: "" 131 | content: "" 132 | """ 133 | 134 | 135 | def write_changes(module, contents, dest): 136 | 137 | tmpfd, tmpfile = tempfile.mkstemp() 138 | f = os.fdopen(tmpfd, 'wb') 139 | f.write(contents) 140 | f.close() 141 | 142 | validate = module.params.get('validate', None) 143 | valid = not validate 144 | if validate: 145 | if "%s" not in validate: 146 | module.fail_json(msg="validate must contain %%s: %s" % (validate)) 147 | (rc, out, err) = module.run_command(validate % tmpfile) 148 | valid = rc == 0 149 | if rc != 0: 150 | module.fail_json(msg='failed to validate: ' 151 | 'rc:%s error:%s' % (rc, err)) 152 | if valid: 153 | module.atomic_move(tmpfile, dest) 154 | 155 | 156 | def check_file_attrs(module, changed, message): 157 | 158 | file_args = module.load_file_common_arguments(module.params) 159 | if module.set_file_attributes_if_different(file_args, False): 160 | 161 | if changed: 162 | message += " and " 163 | changed = True 164 | message += "ownership, perms or SE linux context changed" 165 | 166 | return message, changed 167 | 168 | 169 | def main(): 170 | module = AnsibleModule( 171 | argument_spec=dict( 172 | dest=dict(required=True, aliases=['name', 'destfile']), 173 | state=dict(default='present', choices=['absent', 'present']), 174 | marker=dict(default='# {mark} ANSIBLE MANAGED BLOCK', type='str'), 175 | block=dict(default='', type='str', aliases=['content']), 176 | insertafter=dict(default=None), 177 | insertbefore=dict(default=None), 178 | create=dict(default=False, type='bool'), 179 | backup=dict(default=False, type='bool'), 180 | validate=dict(default=None, type='str'), 181 | ), 182 | mutually_exclusive=[['insertbefore', 'insertafter']], 183 | add_file_common_args=True, 184 | supports_check_mode=True 185 | ) 186 | 187 | params = module.params 188 | dest = os.path.expanduser(params['dest']) 189 | if module.boolean(params.get('follow', None)): 190 | dest = os.path.realpath(dest) 191 | 192 | if os.path.isdir(dest): 193 | module.fail_json(rc=256, 194 | msg='Destination %s is a directory !' % dest) 195 | 196 | if not os.path.exists(dest): 197 | if not module.boolean(params['create']): 198 | module.fail_json(rc=257, 199 | msg='Destination %s does not exist !' % dest) 200 | original = None 201 | lines = [] 202 | else: 203 | f = open(dest, 'rb') 204 | original = f.read() 205 | f.close() 206 | lines = original.splitlines() 207 | 208 | insertbefore = params['insertbefore'] 209 | insertafter = params['insertafter'] 210 | block = params['block'] 211 | marker = params['marker'] 212 | present = params['state'] == 'present' 213 | 214 | if insertbefore is None and insertafter is None: 215 | insertafter = 'EOF' 216 | 217 | if insertafter not in (None, 'EOF'): 218 | insertre = re.compile(insertafter) 219 | elif insertbefore not in (None, 'BOF'): 220 | insertre = re.compile(insertbefore) 221 | else: 222 | insertre = None 223 | 224 | marker0 = re.sub(r'{mark}', 'BEGIN', marker) 225 | marker1 = re.sub(r'{mark}', 'END', marker) 226 | if present and block: 227 | # Escape seqeuences like '\n' need to be handled in Ansible 1.x 228 | if ANSIBLE_VERSION.startswith('1.'): 229 | block = re.sub('', block, '') 230 | blocklines = [marker0] + block.splitlines() + [marker1] 231 | else: 232 | blocklines = [] 233 | 234 | n0 = n1 = None 235 | for i, line in enumerate(lines): 236 | if line.startswith(marker0): 237 | n0 = i 238 | if line.startswith(marker1): 239 | n1 = i 240 | 241 | if None in (n0, n1): 242 | n0 = None 243 | if insertre is not None: 244 | for i, line in enumerate(lines): 245 | if insertre.search(line): 246 | n0 = i 247 | if n0 is None: 248 | n0 = len(lines) 249 | elif insertafter is not None: 250 | n0 += 1 251 | elif insertbefore is not None: 252 | n0 = 0 # insertbefore=BOF 253 | else: 254 | n0 = len(lines) # insertafter=EOF 255 | elif n0 < n1: 256 | lines[n0:n1+1] = [] 257 | else: 258 | lines[n1:n0+1] = [] 259 | n0 = n1 260 | 261 | lines[n0:n0] = blocklines 262 | 263 | if lines: 264 | result = '\n'.join(lines)+'\n' 265 | else: 266 | result = '' 267 | if original == result: 268 | msg = '' 269 | changed = False 270 | elif original is None: 271 | msg = 'File created' 272 | changed = True 273 | elif not blocklines: 274 | msg = 'Block removed' 275 | changed = True 276 | else: 277 | msg = 'Block inserted' 278 | changed = True 279 | 280 | if changed and not module.check_mode: 281 | if module.boolean(params['backup']) and os.path.exists(dest): 282 | module.backup_local(dest) 283 | write_changes(module, result, dest) 284 | 285 | msg, changed = check_file_attrs(module, changed, msg) 286 | module.exit_json(changed=changed, msg=msg) 287 | 288 | # import module snippets 289 | from ansible.module_utils.basic import * 290 | from ansible.module_utils.splitter import * 291 | if __name__ == '__main__': 292 | main() 293 | --------------------------------------------------------------------------------