Merge branch '13301-cwl-resource-scatter' closes #13301
[arvados.git] / services / nodemanager / arvnodeman / config.py
1 #!/usr/bin/env python
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: AGPL-3.0
5
6 from __future__ import absolute_import, print_function
7
8 import ConfigParser
9 import importlib
10 import logging
11 import sys
12
13 import arvados
14 import httplib2
15 import pykka
16 from apiclient import errors as apierror
17
18 from .baseactor import BaseNodeManagerActor
19
20 from libcloud.common.types import LibcloudError
21 from libcloud.common.exceptions import BaseHTTPError
22
23 # IOError is the base class for socket.error, ssl.SSLError, and friends.
24 # It seems like it hits the sweet spot for operations we want to retry:
25 # it's low-level, but unlikely to catch code bugs.
26 NETWORK_ERRORS = (IOError,)
27 ARVADOS_ERRORS = NETWORK_ERRORS + (apierror.Error,)
28 CLOUD_ERRORS = NETWORK_ERRORS + (LibcloudError, BaseHTTPError)
29
30 actor_class = BaseNodeManagerActor
31
32 class NodeManagerConfig(ConfigParser.SafeConfigParser):
33     """Node Manager Configuration class.
34
35     This a standard Python ConfigParser, with additional helper methods to
36     create objects instantiated with configuration information.
37     """
38
39     LOGGING_NONLEVELS = frozenset(['file'])
40
41     def __init__(self, *args, **kwargs):
42         # Can't use super() because SafeConfigParser is an old-style class.
43         ConfigParser.SafeConfigParser.__init__(self, *args, **kwargs)
44         for sec_name, settings in {
45             'Arvados': {'insecure': 'no',
46                         'timeout': '15',
47                         'jobs_queue': 'yes',
48                         'slurm_queue': 'yes'
49                     },
50             'Daemon': {'min_nodes': '0',
51                        'max_nodes': '1',
52                        'poll_time': '60',
53                        'max_poll_time': '300',
54                        'poll_stale_after': '600',
55                        'max_total_price': '0',
56                        'boot_fail_after': str(sys.maxint),
57                        'node_stale_after': str(60 * 60 * 2),
58                        'watchdog': '600',
59                        'node_mem_scaling': '0.95'},
60             'Manage': {'address': '127.0.0.1',
61                        'port': '-1',
62                        'ManagementToken': ''},
63             'Logging': {'file': '/dev/stderr',
64                         'level': 'WARNING'}
65         }.iteritems():
66             if not self.has_section(sec_name):
67                 self.add_section(sec_name)
68             for opt_name, value in settings.iteritems():
69                 if not self.has_option(sec_name, opt_name):
70                     self.set(sec_name, opt_name, value)
71
72     def get_section(self, section, transformer=None):
73         result = self._dict()
74         for key, value in self.items(section):
75             if transformer is not None:
76                 try:
77                     value = transformer(value)
78                 except (TypeError, ValueError):
79                     pass
80             result[key] = value
81         return result
82
83     def log_levels(self):
84         return {key: getattr(logging, self.get('Logging', key).upper())
85                 for key in self.options('Logging')
86                 if key not in self.LOGGING_NONLEVELS}
87
88     def dispatch_classes(self):
89         mod_name = 'arvnodeman.computenode.dispatch'
90         if self.has_option('Daemon', 'dispatcher'):
91             mod_name = '{}.{}'.format(mod_name,
92                                       self.get('Daemon', 'dispatcher'))
93         module = importlib.import_module(mod_name)
94         return (module.ComputeNodeSetupActor,
95                 module.ComputeNodeShutdownActor,
96                 module.ComputeNodeUpdateActor,
97                 module.ComputeNodeMonitorActor)
98
99     def new_arvados_client(self):
100         if self.has_option('Daemon', 'certs_file'):
101             certs_file = self.get('Daemon', 'certs_file')
102         else:
103             certs_file = None
104         insecure = self.getboolean('Arvados', 'insecure')
105         http = httplib2.Http(timeout=self.getint('Arvados', 'timeout'),
106                              ca_certs=certs_file,
107                              disable_ssl_certificate_validation=insecure)
108         return arvados.api(version='v1',
109                            host=self.get('Arvados', 'host'),
110                            token=self.get('Arvados', 'token'),
111                            insecure=insecure,
112                            http=http)
113
114     def new_cloud_client(self):
115         module = importlib.import_module('arvnodeman.computenode.driver.' +
116                                          self.get('Cloud', 'provider'))
117         driver_class = module.ComputeNodeDriver.DEFAULT_DRIVER
118         if self.has_option('Cloud', 'driver_class'):
119             d = self.get('Cloud', 'driver_class').split('.')
120             mod = '.'.join(d[:-1])
121             cls = d[-1]
122             driver_class = importlib.import_module(mod).__dict__[cls]
123         auth_kwargs = self.get_section('Cloud Credentials')
124         if 'timeout' in auth_kwargs:
125             auth_kwargs['timeout'] = int(auth_kwargs['timeout'])
126         return module.ComputeNodeDriver(auth_kwargs,
127                                         self.get_section('Cloud List'),
128                                         self.get_section('Cloud Create'),
129                                         driver_class=driver_class)
130
131     def node_sizes(self, all_sizes):
132         """Finds all acceptable NodeSizes for our installation.
133
134         Returns a list of (NodeSize, kwargs) pairs for each NodeSize object
135         returned by libcloud that matches a size listed in our config file.
136         """
137
138         size_kwargs = {}
139         for sec_name in self.sections():
140             sec_words = sec_name.split(None, 2)
141             if sec_words[0] != 'Size':
142                 continue
143             size_spec = self.get_section(sec_name, int)
144             if 'price' in size_spec:
145                 size_spec['price'] = float(size_spec['price'])
146             size_kwargs[sec_words[1]] = size_spec
147         # EC2 node sizes are identified by id. GCE sizes are identified by name.
148         matching_sizes = []
149         for size in all_sizes:
150             if size.id in size_kwargs:
151                 matching_sizes.append((size, size_kwargs[size.id]))
152             elif size.name in size_kwargs:
153                 matching_sizes.append((size, size_kwargs[size.name]))
154         return matching_sizes
155
156     def shutdown_windows(self):
157         return [int(n)
158                 for n in self.get('Cloud', 'shutdown_windows').split(',')]