reguire gnupg1 on f>=31; sense the system to use gpg1 when installed
[nodemanager.git] / conf_files.py
1 #!/usr/bin/env python3
2
3 # pylint: disable=c0111
4
5 """
6 update local configuration files from PLC
7 """
8
9 import os
10 import time
11 import pwd
12 import grp
13 from hashlib import sha1 as sha
14 import xmlrpc.client
15
16 import curlwrapper
17 import logger
18 import tools
19 from config import Config
20
21 # right after net
22 priority = 2
23
24 class ConfFiles:
25     def __init__(self, noscripts=False):
26         self.config = Config()
27         self.noscripts = noscripts
28         self.data = None
29
30     def checksum(self, path):
31         try:
32             with open(path) as feed:
33                 return sha(feed.read().encode()).digest()
34         except IOError:
35             return None
36
37     def system(self, cmd):
38         if not self.noscripts and cmd:
39             logger.verbose('conf_files: running command %s' % cmd)
40             return tools.fork_as(None, os.system, cmd)
41         else: return 0
42
43     def update_conf_file(self, cf_rec):
44         if not cf_rec['enabled']:
45             return
46         dest = cf_rec['dest']
47         err_cmd = cf_rec['error_cmd']
48         mode = int(cf_rec['file_permissions'], base=8)
49         try:
50             uid = pwd.getpwnam(cf_rec['file_owner'])[2]
51         except:
52             logger.log('conf_files: cannot find user %s -- %s not updated'
53                        %(cf_rec['file_owner'], dest))
54             return
55         try:
56             gid = grp.getgrnam(cf_rec['file_group'])[2]
57         except:
58             logger.log('conf_files: cannot find group %s -- %s not updated'
59                        %(cf_rec['file_group'], dest))
60             return
61         url = 'https://%s/%s' % (self.config.PLC_BOOT_HOST, cf_rec['source'])
62         # set node_id at the end of the request - hacky
63         if tools.node_id():
64             if url.find('?') > 0:
65                 url += '&'
66             else:
67                 url += '?'
68             url += "node_id=%d"%tools.node_id()
69         else:
70             logger.log('conf_files: %s -- WARNING, cannot add node_id to request'
71                        % dest)
72         try:
73             logger.verbose("conf_files: retrieving URL=%s"%url)
74             contents = curlwrapper.retrieve(url, self.config.cacert)
75         except xmlrpc.client.ProtocolError as e:
76             logger.log('conf_files: failed to retrieve %s from %s, skipping' % (dest, url))
77             return
78         if not cf_rec['always_update'] and sha(contents).digest() == self.checksum(dest):
79             return
80         if self.system(cf_rec['preinstall_cmd']):
81             self.system(err_cmd)
82             if not cf_rec['ignore_cmd_errors']:
83                 return
84         logger.log('conf_files: installing file %s from %s' % (dest, url))
85         try:
86             os.makedirs(os.path.dirname(dest))
87         except OSError:
88             pass
89         tools.write_file(dest, lambda f: f.write(contents.decode()),
90                          mode=mode, uidgid=(uid, gid))
91         if self.system(cf_rec['postinstall_cmd']):
92             self.system(err_cmd)
93
94     def run_once(self, data):
95         if "conf_files" in data:
96             for file in data['conf_files']:
97                 try:
98                     self.update_conf_file(file)
99                 except:
100                     logger.log_exc("conf_files: failed to update conf_file")
101         else:
102             logger.log_missing_data("conf_files.run_once", 'conf_files')
103
104
105 def start():
106     pass
107
108
109 def GetSlivers(data, config=None, plc=None):
110     logger.log("conf_files: Running.")
111     instance = ConfFiles()
112     instance.run_once(data)
113     logger.log("conf_files: Done.")
114
115
116 def main():
117     from argparse import ArgumentParser
118     from plcapi import PLCAPI
119
120     parser = ArgumentParser()
121     parser.add_argument('-f', '--config', action='store', dest='config',
122                         default='/etc/planetlab/plc_config',
123                         help='PLC configuration file')
124     parser.add_argument('-k', '--session', action='store', dest='session',
125                         default='/etc/planetlab/session',
126                         help='API session key (or file)')
127     parser.add_argument('--noscripts', action='store_true', dest='noscripts',
128                         default=False,
129                         help='Do not run pre- or post-install scripts')
130     parser.add_argument('--max-attempts', action='store', dest='max_attempts',
131                         default=10,
132                         help='Max number of attempts')
133     parser.add_argument('--period', action='store', dest='period',
134                         help='Time in seconds to wait between attempts')
135     args = parser.parse_args()
136
137     # Load /etc/planetlab/plc_config
138     config = Config(args.config)
139
140     # Load /etc/planetlab/session
141     if os.path.exists(args.session):
142         with open(args.session) as feed:
143             session = feed.read().strip()
144     else:
145         session = args.session
146
147     # loop until it succeeds once
148     # this is a change that comes with python3/fedora29 in late 2018,
149     # because although the conf_files service is defined to systemd
150     # as a dependency of the network, it triggers too early
151     # at a point where eth0 is not ready
152
153     # Initialize XML-RPC client
154     attempts = 0
155     while True:
156         try:
157             plc = PLCAPI(config.plc_api_uri, config.cacert, auth=session)
158             data = plc.GetSlivers()
159             instance = ConfFiles(args.noscripts)
160             instance.run_once(data)
161             return 0
162         except Exception as exc:
163             logger.log_exc("Could not receive GetSlivers() from PLC")
164             attempts += 1
165             if attempts >= args.max_attempts:
166                 return 1
167             logger.log("Waiting for {}s before trying again".format(args.period))
168             time.sleep(args.period)
169
170
171 if __name__ == '__main__':
172     main()