forked from vmware-archive/saltdocker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitfs.py
3180 lines (2928 loc) · 122 KB
/
gitfs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
'''
Classes which provide the shared base for GitFS, git_pillar, and winrepo
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import contextlib
import copy
import errno
import fnmatch
import glob
import hashlib
import logging
import os
import shlex
import shutil
import stat
import subprocess
import sys
import time
import tornado.ioloop
import weakref
from datetime import datetime
# Import salt libs
import salt.utils.configparser
import salt.utils.data
import salt.utils.files
import salt.utils.gzip_util
import salt.utils.hashutils
import salt.utils.itertools
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.url
import salt.utils.user
import salt.utils.versions
import salt.fileserver
from salt.config import DEFAULT_MASTER_OPTS as _DEFAULT_MASTER_OPTS
from salt.utils.odict import OrderedDict
from salt.utils.process import os_is_running as pid_exists
from salt.exceptions import (
FileserverConfigError,
GitLockError,
get_error_message
)
from salt.utils.event import tagify
from salt.utils.versions import LooseVersion as _LooseVersion
# Import third party libs
from salt.ext import six
VALID_REF_TYPES = _DEFAULT_MASTER_OPTS['gitfs_ref_types']
# Optional per-remote params that can only be used on a per-remote basis, and
# thus do not have defaults in salt/config.py.
PER_REMOTE_ONLY = ('name',)
# Params which are global only and cannot be overridden for a single remote.
GLOBAL_ONLY = ()
SYMLINK_RECURSE_DEPTH = 100
# Auth support (auth params can be global or per-remote, too)
AUTH_PROVIDERS = ('pygit2',)
AUTH_PARAMS = ('user', 'password', 'pubkey', 'privkey', 'passphrase',
'insecure_auth')
# GitFS only: params which can be overridden for a single saltenv. Aside from
# 'ref', this must be a subset of the per-remote params passed to the
# constructor for the GitProvider subclasses.
PER_SALTENV_PARAMS = ('mountpoint', 'root', 'ref')
_RECOMMEND_GITPYTHON = (
'GitPython is installed, you may wish to set %s_provider to '
'\'gitpython\' to use GitPython for %s support.'
)
_RECOMMEND_PYGIT2 = (
'pygit2 is installed, you may wish to set %s_provider to '
'\'pygit2\' to use pygit2 for for %s support.'
)
_INVALID_REPO = (
'Cache path %s (corresponding remote: %s) exists but is not a valid '
'git repository. You will need to manually delete this directory on the '
'master to continue to use this %s remote.'
)
log = logging.getLogger(__name__)
# pylint: disable=import-error
try:
import git
import gitdb
GITPYTHON_VERSION = _LooseVersion(git.__version__)
except Exception:
GITPYTHON_VERSION = None
try:
# Squelch warning on cent7 due to them upgrading cffi
import warnings
with warnings.catch_warnings():
warnings.simplefilter('ignore')
import pygit2
PYGIT2_VERSION = _LooseVersion(pygit2.__version__)
LIBGIT2_VERSION = _LooseVersion(pygit2.LIBGIT2_VERSION)
# Work around upstream bug where bytestrings were being decoded using the
# default encoding (which is usually ascii on Python 2). This was fixed
# on 2 Feb 2018, so releases prior to 0.26.3 will need a workaround.
if PYGIT2_VERSION <= _LooseVersion('0.26.3'):
try:
import pygit2.ffi
import pygit2.remote
except ImportError:
# If we couldn't import these, then we're using an old enough
# version where ffi isn't in use and this workaround would be
# useless.
pass
else:
def __maybe_string(ptr):
if not ptr:
return None
return pygit2.ffi.string(ptr).decode('utf-8')
pygit2.remote.maybe_string = __maybe_string
# Older pygit2 releases did not raise a specific exception class, this
# try/except makes Salt's exception catching work on any supported release.
try:
GitError = pygit2.errors.GitError
except AttributeError:
GitError = Exception
except Exception as exc:
# Exceptions other than ImportError can be raised in cases where there is a
# problem with cffi (such as when python-cffi is upgraded and pygit2 tries
# to rebuild itself against the newer cffi). Therefore, we simply will
# catch a generic exception, and log the exception if it is anything other
# than an ImportError.
PYGIT2_VERSION = None
LIBGIT2_VERSION = None
if not isinstance(exc, ImportError):
log.exception('Failed to import pygit2')
# pylint: enable=import-error
# Minimum versions for backend providers
GITPYTHON_MINVER = _LooseVersion('0.3')
PYGIT2_MINVER = _LooseVersion('0.20.3')
LIBGIT2_MINVER = _LooseVersion('0.20.0')
def enforce_types(key, val):
'''
Force params to be strings unless they should remain a different type
'''
non_string_params = {
'ssl_verify': bool,
'insecure_auth': bool,
'disable_saltenv_mapping': bool,
'env_whitelist': 'stringlist',
'env_blacklist': 'stringlist',
'saltenv_whitelist': 'stringlist',
'saltenv_blacklist': 'stringlist',
'refspecs': 'stringlist',
'ref_types': 'stringlist',
'update_interval': int,
}
def _find_global(key):
for item in non_string_params:
try:
if key.endswith('_' + item):
ret = item
break
except TypeError:
if key.endswith('_' + six.text_type(item)):
ret = item
break
else:
ret = None
return ret
if key not in non_string_params:
key = _find_global(key)
if key is None:
return six.text_type(val)
expected = non_string_params[key]
if expected == 'stringlist':
if not isinstance(val, (six.string_types, list)):
val = six.text_type(val)
if isinstance(val, six.string_types):
return [x.strip() for x in val.split(',')]
return [six.text_type(x) for x in val]
else:
try:
return expected(val)
except Exception as exc:
log.error(
'Failed to enforce type for key=%s with val=%s, falling back '
'to a string', key, val
)
return six.text_type(val)
def failhard(role):
'''
Fatal configuration issue, raise an exception
'''
raise FileserverConfigError('Failed to load {0}'.format(role))
class GitProvider(object):
'''
Base class for gitfs/git_pillar provider classes. Should never be used
directly.
self.provider should be set in the sub-class' __init__ function before
invoking the parent class' __init__.
'''
def __init__(self, opts, remote, per_remote_defaults, per_remote_only,
override_params, cache_root, role='gitfs'):
self.opts = opts
self.role = role
self.global_saltenv = salt.utils.data.repack_dictlist(
self.opts.get('{0}_saltenv'.format(self.role), []),
strict=True,
recurse=True,
key_cb=six.text_type,
val_cb=lambda x, y: six.text_type(y))
self.conf = copy.deepcopy(per_remote_defaults)
# Remove the 'salt://' from the beginning of any globally-defined
# per-saltenv mountpoints
for saltenv, saltenv_conf in six.iteritems(self.global_saltenv):
if 'mountpoint' in saltenv_conf:
self.global_saltenv[saltenv]['mountpoint'] = \
salt.utils.url.strip_proto(
self.global_saltenv[saltenv]['mountpoint']
)
per_remote_collisions = [x for x in override_params
if x in per_remote_only]
if per_remote_collisions:
log.critical(
'The following parameter names are restricted to per-remote '
'use only: %s. This is a bug, please report it.',
', '.join(per_remote_collisions)
)
try:
valid_per_remote_params = override_params + per_remote_only
except TypeError:
valid_per_remote_params = \
list(override_params) + list(per_remote_only)
if isinstance(remote, dict):
self.id = next(iter(remote))
self.get_url()
per_remote_conf = salt.utils.data.repack_dictlist(
remote[self.id],
strict=True,
recurse=True,
key_cb=six.text_type,
val_cb=enforce_types)
if not per_remote_conf:
log.critical(
'Invalid per-remote configuration for %s remote \'%s\'. '
'If no per-remote parameters are being specified, there '
'may be a trailing colon after the URL, which should be '
'removed. Check the master configuration file.',
self.role, self.id
)
failhard(self.role)
if self.role == 'git_pillar' \
and self.branch != '__env__' and 'base' in per_remote_conf:
log.critical(
'Invalid per-remote configuration for %s remote \'%s\'. '
'base can only be specified if __env__ is specified as the branch name.',
self.role, self.id
)
failhard(self.role)
per_remote_errors = False
for param in (x for x in per_remote_conf
if x not in valid_per_remote_params):
per_remote_errors = True
if param in AUTH_PARAMS \
and self.provider not in AUTH_PROVIDERS:
msg = (
'{0} authentication parameter \'{1}\' (from remote '
'\'{2}\') is only supported by the following '
'provider(s): {3}. Current {0}_provider is \'{4}\'.'
.format(
self.role,
param,
self.id,
', '.join(AUTH_PROVIDERS),
self.provider
)
)
if self.role == 'gitfs':
msg += (
'See the GitFS Walkthrough in the Salt '
'documentation for further information.'
)
log.critical(msg)
else:
msg = (
'Invalid {0} configuration parameter \'{1}\' in '
'remote \'{2}\'. Valid parameters are: {3}.'.format(
self.role,
param,
self.id,
', '.join(valid_per_remote_params)
)
)
if self.role == 'gitfs':
msg += (
' See the GitFS Walkthrough in the Salt '
'documentation for further information.'
)
log.critical(msg)
if per_remote_errors:
failhard(self.role)
self.conf.update(per_remote_conf)
else:
self.id = remote
self.get_url()
# Winrepo doesn't support the 'root' option, but it still must be part
# of the GitProvider object because other code depends on it. Add it as
# an empty string.
if 'root' not in self.conf:
self.conf['root'] = ''
if self.role == 'winrepo' and 'name' not in self.conf:
# Ensure that winrepo has the 'name' parameter set if it wasn't
# provided. Default to the last part of the URL, minus the .git if
# it is present.
self.conf['name'] = self.url.rsplit('/', 1)[-1]
# Remove trailing .git from name
if self.conf['name'].lower().endswith('.git'):
self.conf['name'] = self.conf['name'][:-4]
if 'mountpoint' in self.conf:
# Remove the 'salt://' from the beginning of the mountpoint, as
# well as any additional leading/trailing slashes
self.conf['mountpoint'] = \
salt.utils.url.strip_proto(self.conf['mountpoint']).strip('/')
else:
# For providers which do not use a mountpoint, assume the
# filesystem is mounted at the root of the fileserver.
self.conf['mountpoint'] = ''
if 'saltenv' not in self.conf:
self.conf['saltenv'] = {}
else:
for saltenv, saltenv_conf in six.iteritems(self.conf['saltenv']):
if 'mountpoint' in saltenv_conf:
saltenv_ptr = self.conf['saltenv'][saltenv]
saltenv_ptr['mountpoint'] = \
salt.utils.url.strip_proto(saltenv_ptr['mountpoint'])
for key, val in six.iteritems(self.conf):
if key not in PER_SALTENV_PARAMS and not hasattr(self, key):
setattr(self, key, val)
for key in PER_SALTENV_PARAMS:
if key != 'ref':
setattr(self, '_' + key, self.conf[key])
self.add_conf_overlay(key)
if not hasattr(self, 'refspecs'):
# This was not specified as a per-remote overrideable parameter
# when instantiating an instance of a GitBase subclass. Make sure
# that we set this attribute so we at least have a sane default and
# are able to fetch.
key = '{0}_refspecs'.format(self.role)
try:
default_refspecs = _DEFAULT_MASTER_OPTS[key]
except KeyError:
log.critical(
'The \'%s\' option has no default value in '
'salt/config/__init__.py.', key
)
failhard(self.role)
setattr(self, 'refspecs', default_refspecs)
log.debug(
'The \'refspecs\' option was not explicitly defined as a '
'configurable parameter. Falling back to %s for %s remote '
'\'%s\'.', default_refspecs, self.role, self.id
)
for item in ('env_whitelist', 'env_blacklist'):
val = getattr(self, item, None)
if val:
salt.utils.versions.warn_until(
'Neon',
'The gitfs_{0} config option (and {0} per-remote config '
'option) have been renamed to gitfs_salt{0} (and '
'salt{0}). Please update your configuration.'.format(item)
)
setattr(self, 'salt{0}'.format(item), val)
# Discard the conf dictionary since we have set all of the config
# params as attributes
delattr(self, 'conf')
# Normalize components of the ref_types configuration and check for
# invalid configuration.
if hasattr(self, 'ref_types'):
self.ref_types = [x.lower() for x in self.ref_types]
invalid_ref_types = [x for x in self.ref_types
if x not in VALID_REF_TYPES]
if invalid_ref_types:
log.critical(
'The following ref_types for %s remote \'%s\' are '
'invalid: %s. The supported values are: %s',
self.role,
self.id,
', '.join(invalid_ref_types),
', '.join(VALID_REF_TYPES),
)
failhard(self.role)
if not isinstance(self.url, six.string_types):
log.critical(
'Invalid %s remote \'%s\'. Remotes must be strings, you '
'may need to enclose the URL in quotes', self.role, self.id
)
failhard(self.role)
hash_type = getattr(hashlib, self.opts.get('hash_type', 'md5'))
if six.PY3:
# We loaded this data from yaml configuration files, so, its safe
# to use UTF-8
self.hash = hash_type(self.id.encode('utf-8')).hexdigest()
else:
self.hash = hash_type(self.id).hexdigest()
self.cachedir_basename = getattr(self, 'name', self.hash)
self.cachedir = salt.utils.path.join(cache_root, self.cachedir_basename)
self.linkdir = salt.utils.path.join(cache_root,
'links',
self.cachedir_basename)
if not os.path.isdir(self.cachedir):
os.makedirs(self.cachedir)
try:
self.new = self.init_remote()
except Exception as exc:
msg = ('Exception caught while initializing {0} remote \'{1}\': '
'{2}'.format(self.role, self.id, exc))
if isinstance(self, GitPython):
msg += ' Perhaps git is not available.'
log.critical(msg, exc_info=True)
failhard(self.role)
def _get_envs_from_ref_paths(self, refs):
'''
Return the names of remote refs (stripped of the remote name) and tags
which are map to the branches and tags.
'''
def _check_ref(env_set, rname):
'''
Add the appropriate saltenv(s) to the set
'''
if rname in self.saltenv_revmap:
env_set.update(self.saltenv_revmap[rname])
else:
if rname == self.base:
env_set.add('base')
elif not self.disable_saltenv_mapping:
env_set.add(rname)
use_branches = 'branch' in self.ref_types
use_tags = 'tag' in self.ref_types
ret = set()
if salt.utils.stringutils.is_hex(self.base):
# gitfs_base or per-saltenv 'base' may point to a commit ID, which
# would not show up in the refs. Make sure we include it.
ret.add('base')
for ref in salt.utils.data.decode(refs):
if ref.startswith('refs/'):
ref = ref[5:]
rtype, rname = ref.split('/', 1)
if rtype == 'remotes' and use_branches:
parted = rname.partition('/')
rname = parted[2] if parted[2] else parted[0]
_check_ref(ret, rname)
elif rtype == 'tags' and use_tags:
_check_ref(ret, rname)
return ret
def _get_lock_file(self, lock_type='update'):
return salt.utils.path.join(self.gitdir, lock_type + '.lk')
@classmethod
def add_conf_overlay(cls, name):
'''
Programmatically determine config value based on the desired saltenv
'''
def _getconf(self, tgt_env='base'):
strip_sep = lambda x: x.rstrip(os.sep) \
if name in ('root', 'mountpoint') \
else x
if self.role != 'gitfs':
return strip_sep(getattr(self, '_' + name))
# Get saltenv-specific configuration
saltenv_conf = self.saltenv.get(tgt_env, {})
if name == 'ref':
def _get_per_saltenv(tgt_env):
if name in saltenv_conf:
return saltenv_conf[name]
elif tgt_env in self.global_saltenv \
and name in self.global_saltenv[tgt_env]:
return self.global_saltenv[tgt_env][name]
else:
return None
# Return the all_saltenvs branch/tag if it is configured
per_saltenv_ref = _get_per_saltenv(tgt_env)
try:
all_saltenvs_ref = self.all_saltenvs
if per_saltenv_ref and all_saltenvs_ref != per_saltenv_ref:
log.debug(
'The per-saltenv configuration has mapped the '
'\'%s\' branch/tag to saltenv \'%s\' for %s '
'remote \'%s\', but this remote has '
'all_saltenvs set to \'%s\'. The per-saltenv '
'mapping will be ignored in favor of \'%s\'.',
per_saltenv_ref, tgt_env, self.role, self.id,
all_saltenvs_ref, all_saltenvs_ref
)
return all_saltenvs_ref
except AttributeError:
# all_saltenvs not configured for this remote
pass
if tgt_env == 'base':
return self.base
elif self.disable_saltenv_mapping:
if per_saltenv_ref is None:
log.debug(
'saltenv mapping is diabled for %s remote \'%s\' '
'and saltenv \'%s\' is not explicitly mapped',
self.role, self.id, tgt_env
)
return per_saltenv_ref
else:
return per_saltenv_ref or tgt_env
if name in saltenv_conf:
return strip_sep(saltenv_conf[name])
elif tgt_env in self.global_saltenv \
and name in self.global_saltenv[tgt_env]:
return strip_sep(self.global_saltenv[tgt_env][name])
else:
return strip_sep(getattr(self, '_' + name))
setattr(cls, name, _getconf)
def check_root(self):
'''
Check if the relative root path exists in the checked-out copy of the
remote. Return the full path to that relative root if it does exist,
otherwise return None.
'''
# No need to pass an environment to self.root() here since per-saltenv
# configuration is a gitfs-only feature and check_root() is not used
# for gitfs.
root_dir = salt.utils.path.join(self.cachedir, self.root()).rstrip(os.sep)
if os.path.isdir(root_dir):
return root_dir
log.error(
'Root path \'%s\' not present in %s remote \'%s\', '
'skipping.', self.root(), self.role, self.id
)
return None
def clean_stale_refs(self):
'''
Remove stale refs so that they are no longer seen as fileserver envs
'''
cleaned = []
cmd_str = 'git remote prune origin'
# Attempt to force all output to plain ascii english, which is what some parsing code
# may expect.
# According to stackoverflow (http://goo.gl/l74GC8), we are setting LANGUAGE as well
# just to be sure.
env = os.environ.copy()
env[b"LANGUAGE"] = b"C"
env[b"LC_ALL"] = b"C"
cmd = subprocess.Popen(
shlex.split(cmd_str),
close_fds=not salt.utils.platform.is_windows(),
cwd=os.path.dirname(self.gitdir),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
output = cmd.communicate()[0]
if six.PY3:
output = output.decode(__salt_system_encoding__)
if cmd.returncode != 0:
log.warning(
'Failed to prune stale branches for %s remote \'%s\'. '
'Output from \'%s\' follows:\n%s',
self.role, self.id, cmd_str, output
)
else:
marker = ' * [pruned] '
for line in salt.utils.itertools.split(output, '\n'):
if line.startswith(marker):
cleaned.append(line[len(marker):].strip())
if cleaned:
log.debug(
'%s pruned the following stale refs: %s',
self.role, ', '.join(cleaned)
)
return cleaned
def clear_lock(self, lock_type='update'):
'''
Clear update.lk
'''
lock_file = self._get_lock_file(lock_type=lock_type)
def _add_error(errlist, exc):
msg = ('Unable to remove update lock for {0} ({1}): {2} '
.format(self.url, lock_file, exc))
log.debug(msg)
errlist.append(msg)
success = []
failed = []
try:
os.remove(lock_file)
except OSError as exc:
if exc.errno == errno.ENOENT:
# No lock file present
pass
elif exc.errno == errno.EISDIR:
# Somehow this path is a directory. Should never happen
# unless some wiseguy manually creates a directory at this
# path, but just in case, handle it.
try:
shutil.rmtree(lock_file)
except OSError as exc:
_add_error(failed, exc)
else:
_add_error(failed, exc)
else:
msg = 'Removed {0} lock for {1} remote \'{2}\''.format(
lock_type,
self.role,
self.id
)
log.debug(msg)
success.append(msg)
return success, failed
def enforce_git_config(self):
'''
For the config options which need to be maintained in the git config,
ensure that the git config file is configured as desired.
'''
git_config = os.path.join(self.gitdir, 'config')
conf = salt.utils.configparser.GitConfigParser()
if not conf.read(git_config):
log.error('Failed to read from git config file %s', git_config)
else:
# We are currently enforcing the following git config items:
# 1. Fetch URL
# 2. refspecs used in fetch
# 3. http.sslVerify
conf_changed = False
remote_section = 'remote "origin"'
# 1. URL
try:
url = conf.get(remote_section, 'url')
except salt.utils.configparser.NoSectionError:
# First time we've init'ed this repo, we need to add the
# section for the remote to the git config
conf.add_section(remote_section)
conf_changed = True
url = None
log.debug(
'Current fetch URL for %s remote \'%s\': %s (desired: %s)',
self.role, self.id, url, self.url
)
if url != self.url:
conf.set(remote_section, 'url', self.url)
log.debug(
'Fetch URL for %s remote \'%s\' set to %s',
self.role, self.id, self.url
)
conf_changed = True
# 2. refspecs
try:
refspecs = sorted(
conf.get(remote_section, 'fetch', as_list=True))
except salt.utils.configparser.NoOptionError:
# No 'fetch' option present in the remote section. Should never
# happen, but if it does for some reason, don't let it cause a
# traceback.
refspecs = []
desired_refspecs = sorted(self.refspecs)
log.debug(
'Current refspecs for %s remote \'%s\': %s (desired: %s)',
self.role, self.id, refspecs, desired_refspecs
)
if refspecs != desired_refspecs:
conf.set_multivar(remote_section, 'fetch', self.refspecs)
log.debug(
'Refspecs for %s remote \'%s\' set to %s',
self.role, self.id, desired_refspecs
)
conf_changed = True
# 3. http.sslVerify
try:
ssl_verify = conf.get('http', 'sslVerify')
except salt.utils.configparser.NoSectionError:
conf.add_section('http')
ssl_verify = None
except salt.utils.configparser.NoOptionError:
ssl_verify = None
desired_ssl_verify = six.text_type(self.ssl_verify).lower()
log.debug(
'Current http.sslVerify for %s remote \'%s\': %s (desired: %s)',
self.role, self.id, ssl_verify, desired_ssl_verify
)
if ssl_verify != desired_ssl_verify:
conf.set('http', 'sslVerify', desired_ssl_verify)
log.debug(
'http.sslVerify for %s remote \'%s\' set to %s',
self.role, self.id, desired_ssl_verify
)
conf_changed = True
# Write changes, if necessary
if conf_changed:
with salt.utils.files.fopen(git_config, 'w') as fp_:
conf.write(fp_)
log.debug(
'Config updates for %s remote \'%s\' written to %s',
self.role, self.id, git_config
)
def fetch(self):
'''
Fetch the repo. If the local copy was updated, return True. If the
local copy was already up-to-date, return False.
This function requires that a _fetch() function be implemented in a
sub-class.
'''
try:
with self.gen_lock(lock_type='update'):
log.debug('Fetching %s remote \'%s\'', self.role, self.id)
# Run provider-specific fetch code
return self._fetch()
except GitLockError as exc:
if exc.errno == errno.EEXIST:
log.warning(
'Update lock file is present for %s remote \'%s\', '
'skipping. If this warning persists, it is possible that '
'the update process was interrupted, but the lock could '
'also have been manually set. Removing %s or running '
'\'salt-run cache.clear_git_lock %s type=update\' will '
'allow updates to continue for this remote.',
self.role,
self.id,
self._get_lock_file(lock_type='update'),
self.role,
)
return False
def _lock(self, lock_type='update', failhard=False):
'''
Place a lock file if (and only if) it does not already exist.
'''
try:
fh_ = os.open(self._get_lock_file(lock_type),
os.O_CREAT | os.O_EXCL | os.O_WRONLY)
with os.fdopen(fh_, 'wb'):
# Write the lock file and close the filehandle
os.write(fh_, salt.utils.stringutils.to_bytes(six.text_type(os.getpid())))
except (OSError, IOError) as exc:
if exc.errno == errno.EEXIST:
with salt.utils.files.fopen(self._get_lock_file(lock_type), 'r') as fd_:
try:
pid = int(salt.utils.stringutils.to_unicode(fd_.readline()).rstrip())
except ValueError:
# Lock file is empty, set pid to 0 so it evaluates as
# False.
pid = 0
global_lock_key = self.role + '_global_lock'
lock_file = self._get_lock_file(lock_type=lock_type)
if self.opts[global_lock_key]:
msg = (
'{0} is enabled and {1} lockfile {2} is present for '
'{3} remote \'{4}\'.'.format(
global_lock_key,
lock_type,
lock_file,
self.role,
self.id,
)
)
if pid:
msg += ' Process {0} obtained the lock'.format(pid)
if not pid_exists(pid):
msg += (' but this process is not running. The '
'update may have been interrupted. If '
'using multi-master with shared gitfs '
'cache, the lock may have been obtained '
'by another master.')
log.warning(msg)
if failhard:
six.reraise(*sys.exc_info())
return
elif pid and pid_exists(pid):
log.warning('Process %d has a %s %s lock (%s)',
pid, self.role, lock_type, lock_file)
if failhard:
raise
return
else:
if pid:
log.warning(
'Process %d has a %s %s lock (%s), but this '
'process is not running. Cleaning up lock file.',
pid, self.role, lock_type, lock_file
)
success, fail = self.clear_lock()
if success:
return self._lock(lock_type='update',
failhard=failhard)
elif failhard:
raise
return
else:
msg = 'Unable to set {0} lock for {1} ({2}): {3} '.format(
lock_type,
self.id,
self._get_lock_file(lock_type),
exc
)
log.error(msg, exc_info=True)
raise GitLockError(exc.errno, msg)
msg = 'Set {0} lock for {1} remote \'{2}\''.format(
lock_type,
self.role,
self.id
)
log.debug(msg)
return msg
def lock(self):
'''
Place an lock file and report on the success/failure. This is an
interface to be used by the fileserver runner, so it is hard-coded to
perform an update lock. We aren't using the gen_lock()
contextmanager here because the lock is meant to stay and not be
automatically removed.
'''
success = []
failed = []
try:
result = self._lock(lock_type='update')
except GitLockError as exc:
failed.append(exc.strerror)
else:
if result is not None:
success.append(result)
return success, failed
@contextlib.contextmanager
def gen_lock(self, lock_type='update', timeout=0, poll_interval=0.5):
'''
Set and automatically clear a lock
'''
if not isinstance(lock_type, six.string_types):
raise GitLockError(
errno.EINVAL,
'Invalid lock_type \'{0}\''.format(lock_type)
)
# Make sure that we have a positive integer timeout, otherwise just set
# it to zero.
try:
timeout = int(timeout)
except ValueError:
timeout = 0
else:
if timeout < 0:
timeout = 0
if not isinstance(poll_interval, (six.integer_types, float)) \
or poll_interval < 0:
poll_interval = 0.5
if poll_interval > timeout:
poll_interval = timeout
lock_set = False
try:
time_start = time.time()
while True:
try:
self._lock(lock_type=lock_type, failhard=True)
lock_set = True
yield
# Break out of his loop once we've yielded the lock, to
# avoid continued attempts to iterate and establish lock
break
except (OSError, IOError, GitLockError) as exc:
if not timeout or time.time() - time_start > timeout:
raise GitLockError(exc.errno, exc.strerror)
else:
log.debug(
'A %s lock is already present for %s remote '
'\'%s\', sleeping %f second(s)',
lock_type, self.role, self.id, poll_interval
)
time.sleep(poll_interval)
continue
finally:
if lock_set:
self.clear_lock(lock_type=lock_type)
def init_remote(self):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()
def checkout(self):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()
def dir_list(self, tgt_env):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()
def env_is_exposed(self, tgt_env):
'''
Check if an environment is exposed by comparing it against a whitelist
and blacklist.
'''
return salt.utils.stringutils.check_whitelist_blacklist(
tgt_env,
whitelist=self.saltenv_whitelist,
blacklist=self.saltenv_blacklist,
)
def _fetch(self):
'''
Provider-specific code for fetching, must be implemented in a
sub-class.
'''
raise NotImplementedError()
def envs(self):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()
def file_list(self, tgt_env):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()
def find_file(self, path, tgt_env):
'''
This function must be overridden in a sub-class
'''
raise NotImplementedError()