Skip to content

Commit 3e59c46

Browse files
committed
Files op idempotency (#630)
* Implement idempotency for the `files.download` operation. * Add/remove idempotency reasoning on files link/directory/file operations. * Implement idempotency for the `files.put` operations. This also adds idempotency for the `files.sync`, `files.template`, `server.script`, `server.script_template` operations and also when adding manually defined repositories in `yum.repo`, `dnf.repo` and `zypper.repo`. * Implement idempotency for the `files.line` & `files.replace` operations. * Add non idempotent reasons to server script operations. * Cleanup files fact docstrings. * Fix/add missing default value for `files.Find*` facts. * Fix file upload op API test. * Mock out slow paramiko `LazyFqdn` object in SSH client tests. * Use `del [:]` as replacement for missing list `clear` method in Python 2.
1 parent 7e1cc0e commit 3e59c46

File tree

73 files changed

+217
-159
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+217
-159
lines changed

pyinfra/facts/files.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ class Socket(File):
131131

132132
class Sha1File(FactBase):
133133
'''
134-
Returns a SHA1 hash of a file. Works with both sha1sum and sha1.
134+
Returns a SHA1 hash of a file. Works with both sha1sum and sha1. Returns
135+
``None`` if the file doest not exist.
135136
'''
136137

137138
_regexes = [
@@ -157,7 +158,7 @@ def process(self, output):
157158

158159
class Sha256File(FactBase):
159160
'''
160-
Returns a SHA256 hash of a file.
161+
Returns a SHA256 hash of a file, or ``None`` if the file does not exist.
161162
'''
162163

163164
_regexes = [
@@ -185,7 +186,7 @@ def process(self, output):
185186

186187
class Md5File(FactBase):
187188
'''
188-
Returns an MD5 hash of a file.
189+
Returns an MD5 hash of a file, or ``None`` if the file does not exist.
189190
'''
190191

191192
_regexes = [
@@ -232,9 +233,18 @@ def process(self, output):
232233
return output
233234

234235

235-
class FindFiles(FactBase):
236+
class FindFilesBase(FactBase):
237+
abstract = True
238+
default = list
239+
240+
@staticmethod
241+
def process(output):
242+
return output
243+
244+
245+
class FindFiles(FindFilesBase):
236246
'''
237-
Returns a list of files from a start point, recursively using find.
247+
Returns a list of files from a start path, recursively using find.
238248
'''
239249

240250
@staticmethod
@@ -244,14 +254,10 @@ def command(path):
244254
QuoteString(path),
245255
)
246256

247-
@staticmethod
248-
def process(output):
249-
return output
250-
251257

252-
class FindLinks(FindFiles):
258+
class FindLinks(FindFilesBase):
253259
'''
254-
Returns a list of links from a start point, recursively using find.
260+
Returns a list of links from a start path, recursively using find.
255261
'''
256262

257263
@staticmethod
@@ -262,9 +268,9 @@ def command(path):
262268
)
263269

264270

265-
class FindDirectories(FindFiles):
271+
class FindDirectories(FindFilesBase):
266272
'''
267-
Returns a list of directories from a start point, recursively using find.
273+
Returns a list of directories from a start path, recursively using find.
268274
'''
269275

270276
@staticmethod

pyinfra/operations/files.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def download(
6767
state=None, host=None,
6868
):
6969
'''
70-
Download files from remote locations using curl or wget.
70+
Download files from remote locations using ``curl`` or ``wget``.
7171
7272
+ src: source URL of the file
7373
+ dest: where to save the file
@@ -92,6 +92,8 @@ def download(
9292
'''
9393

9494
info = host.get_fact(File, path=dest)
95+
host_datetime = host.get_fact(Date).replace(tzinfo=None)
96+
9597
# Destination is a directory?
9698
if info is False:
9799
raise OperationError(
@@ -111,8 +113,8 @@ def download(
111113
if cache_time:
112114
# Time on files is not tz-aware, and will be the same tz as the server's time,
113115
# so we can safely remove the tzinfo from the Date fact before comparison.
114-
cache_time = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
115-
if info['mtime'] and info['mtime'] > cache_time:
116+
cache_time = host_datetime - timedelta(seconds=cache_time)
117+
if info['mtime'] and info['mtime'] < cache_time:
116118
download = True
117119

118120
if sha1sum:
@@ -171,6 +173,24 @@ def download(
171173
'|| ( echo {2} && exit 1 )'
172174
), QuoteString(dest), md5sum, QuoteString('MD5 did not match!'))
173175

176+
host.create_fact(
177+
File,
178+
kwargs={'path': dest},
179+
data={'mode': mode, 'group': group, 'user': user, 'mtime': host_datetime},
180+
)
181+
182+
# Remove any checksum facts as we don't know the correct values
183+
for value, fact_cls in (
184+
(sha1sum, Sha1File),
185+
(sha256sum, Sha256File),
186+
(md5sum, Md5File),
187+
):
188+
fact_kwargs = {'path': dest}
189+
if value:
190+
host.create_fact(fact_cls, kwargs=fact_kwargs, data=value)
191+
else:
192+
host.delete_fact(fact_cls, kwargs=fact_kwargs)
193+
174194
else:
175195
host.noop('file {0} has already been downloaded'.format(dest))
176196

@@ -332,6 +352,12 @@ def line(
332352
else:
333353
host.noop('line "{0}" exists in {1}'.format(replace or line, path))
334354

355+
host.create_fact(
356+
FindInFile,
357+
kwargs={'path': path, 'pattern': match_line},
358+
data=[replace or line],
359+
)
360+
335361
# Line(s) exists and we want to remove them, replace with nothing
336362
elif present_lines and not present:
337363
yield sed_replace(
@@ -341,11 +367,18 @@ def line(
341367
interpolate_variables=interpolate_variables,
342368
)
343369

370+
host.delete_fact(
371+
FindInFile,
372+
kwargs={'path': path, 'pattern': match_line},
373+
)
374+
344375
# Line(s) exists and we have want to ensure they're correct
345376
elif present_lines and present:
346377
# If any of lines are different, sed replace them
347378
if replace and any(line != replace for line in present_lines):
348379
yield sed_replace_command
380+
del present_lines[:] # TODO: use .clear() when py3+
381+
present_lines.append(replace)
349382
else:
350383
host.noop('line "{0}" exists in {1}'.format(replace or line, path))
351384

@@ -395,6 +428,11 @@ def replace(
395428
backup=backup,
396429
interpolate_variables=interpolate_variables,
397430
)
431+
host.create_fact(
432+
FindInFile,
433+
kwargs={'path': path, 'pattern': match},
434+
data=[],
435+
)
398436
else:
399437
host.noop('string "{0}" does not exist in {1}'.format(match, path))
400438

@@ -578,10 +616,15 @@ def _create_remote_dir(state, host, remote_filename, user, group):
578616
)
579617

580618

581-
@operation(pipeline_facts={
582-
'file': 'src',
583-
'sha1_file': 'src',
584-
})
619+
@operation(
620+
# We don't (currently) cache the local state, so there's nothing we can
621+
# update to flag the local file as present.
622+
is_idempotent=False,
623+
pipeline_facts={
624+
'file': 'src',
625+
'sha1_file': 'src',
626+
},
627+
)
585628
def get(
586629
src, dest,
587630
add_deploy_dir=True, create_local_dir=False, force=False,
@@ -705,6 +748,8 @@ def put(
705748
if create_remote_dir:
706749
yield _create_remote_dir(state, host, dest, user, group)
707750

751+
local_sum = get_file_sha1(src)
752+
708753
# No remote file, always upload and user/group/mode if supplied
709754
if not remote_file or force:
710755
yield FileUploadCommand(local_file, dest)
@@ -717,7 +762,6 @@ def put(
717762

718763
# File exists, check sum and check user/group/mode if supplied
719764
else:
720-
local_sum = get_file_sha1(src)
721765
remote_sum = host.get_fact(Sha1File, path=dest)
722766

723767
# Check sha1sum, upload if needed
@@ -749,6 +793,14 @@ def put(
749793
if not changed:
750794
host.noop('file {0} is already uploaded'.format(dest))
751795

796+
# Now we've uploaded the file and ensured user/group/mode, update the relevant fact data
797+
host.create_fact(Sha1File, kwargs={'path': dest}, data=local_sum)
798+
host.create_fact(
799+
File,
800+
kwargs={'path': dest},
801+
data={'user': user, 'group': group, 'mode': mode},
802+
)
803+
752804

753805
@operation
754806
def template(

tests/operations/apt.deb/add.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"args": ["http://somewhere.com/something.deb"],
33
"facts": {
4+
"server.Date": "datetime:2015-01-01T00:00:00",
45
"files.File": {
56
"path=_tempfile_": ""
67
},

tests/operations/apt.deb/add_existing.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"args": ["http://somewhere.com/something.deb"],
33
"facts": {
4+
"server.Date": "datetime:2015-01-01T00:00:00",
45
"files.File": {
56
"path=_tempfile_": ""
67
},

tests/operations/apt.deb/download_add.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"args": ["http://somewhere.com/something.deb"],
33
"facts": {
4+
"server.Date": "datetime:2015-01-01T00:00:00",
45
"files.File": {
56
"path=_tempfile_": null
67
},

tests/operations/apt.deb/remove_existing.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"present": false
55
},
66
"facts": {
7+
"server.Date": "datetime:2015-01-01T00:00:00",
78
"files.File": {
89
"path=_tempfile_": ""
910
},

tests/operations/apt.deb/remove_no_exist.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"present": false
55
},
66
"facts": {
7+
"server.Date": "datetime:2015-01-01T00:00:00",
78
"files.File": {
89
"path=_tempfile_": ""
910
},

tests/operations/bsdinit.service/disabled.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@
1616
},
1717
"commands": [
1818
"sed -i.a-timestamp 's/^nginx_enable=.*$//' /etc/rc.conf.local && rm -f /etc/rc.conf.local.a-timestamp"
19-
],
20-
"idempotent": false
19+
]
2120
}

tests/operations/bsdinit.service/enabled.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@
1515
},
1616
"commands": [
1717
"echo 'nginx_enable=\"YES\"' >> /etc/rc.conf.local"
18-
],
19-
"idempotent": false
18+
]
2019
}

tests/operations/dnf.repo/add.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,5 @@
1818
"upload",
1919
"[somerepo]\nname=description\nbaseurl=http://baseurl\nenabled=1\ngpgcheck=0\ngpgkey=test\n",
2020
"/etc/yum.repos.d/somerepo.repo"
21-
]],
22-
"idempotent": false
21+
]]
2322
}

0 commit comments

Comments
 (0)