13804: Fix tests
[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                        'consecutive_idle_count': '2'},
61             'Manage': {'address': '127.0.0.1',
62                        'port': '-1',
63                        'ManagementToken': ''},
64             'Logging': {'file': '/dev/stderr',
65                         'level': 'WARNING'}
66         }.iteritems():
67             if not self.has_section(sec_name):
68                 self.add_section(sec_name)
69             for opt_name, value in settings.iteritems():
70                 if not self.has_option(sec_name, opt_name):
71                     self.set(sec_name, opt_name, value)
72
73     def get_section(self, section, transformer=None):
74         result = self._dict()
75         for key, value in self.items(section):
76             if transformer is not None:
77                 try:
78                     value = transformer(value)
79                 except (TypeError, ValueError):
80                     pass
81             result[key] = value
82         return result
83
84     def log_levels(self):
85         return {key: getattr(logging, self.get('Logging', key).upper())
86                 for key in self.options('Logging')
87                 if key not in self.LOGGING_NONLEVELS}
88
89     def dispatch_classes(self):
90         mod_name = 'arvnodeman.computenode.dispatch'
91         if self.has_option('Daemon', 'dispatcher'):
92             mod_name = '{}.{}'.format(mod_name,
93                                       self.get('Daemon', 'dispatcher'))
94         module = importlib.import_module(mod_name)
95         return (module.ComputeNodeSetupActor,
96                 module.ComputeNodeShutdownActor,
97                 module.ComputeNodeUpdateActor,
98                 module.ComputeNodeMonitorActor)
99
100     def new_arvados_client(self):
101         if self.has_option('Daemon', 'certs_file'):
102             certs_file = self.get('Daemon', 'certs_file')
103         else:
104             certs_file = None
105         insecure = self.getboolean('Arvados', 'insecure')
106         http = httplib2.Http(timeout=self.getint('Arvados', 'timeout'),
107                              ca_certs=certs_file,
108                              disable_ssl_certificate_validation=insecure)
109         return arvados.api(version='v1',
110                            host=self.get('Arvados', 'host'),
111                            token=self.get('Arvados', 'token'),
112                            insecure=insecure,
113                            http=http)
114
115     def new_cloud_client(self):
116         module = importlib.import_module('arvnodeman.computenode.driver.' +
117                                          self.get('Cloud', 'provider'))
118         driver_class = module.ComputeNodeDriver.DEFAULT_DRIVER
119         if self.has_option('Cloud', 'driver_class'):
120             d = self.get('Cloud', 'driver_class').split('.')
121             mod = '.'.join(d[:-1])
122             cls = d[-1]
123             driver_class = importlib.import_module(mod).__dict__[cls]
124         auth_kwargs = self.get_section('Cloud Credentials')
125         if 'timeout' in auth_kwargs:
126             auth_kwargs['timeout'] = int(auth_kwargs['timeout'])
127         return module.ComputeNodeDriver(auth_kwargs,
128                                         self.get_section('Cloud List'),
129                                         self.get_section('Cloud Create'),
130                                         driver_class=driver_class)
131
132     def node_sizes(self, all_sizes):
133         """Finds all acceptable NodeSizes for our installation.
134
135         Returns a list of (NodeSize, kwargs) pairs for each NodeSize object
136         returned by libcloud that matches a size listed in our config file.
137         """
138
139         size_kwargs = {}
140         for sec_name in self.sections():
141             sec_words = sec_name.split(None, 2)
142             if sec_words[0] != 'Size':
143                 continue
144             size_spec = self.get_section(sec_name, int)
145             if 'price' in size_spec:
146                 size_spec['price'] = float(size_spec['price'])
147             size_kwargs[sec_words[1]] = size_spec
148         # EC2 node sizes are identified by id. GCE sizes are identified by name.
149         matching_sizes = []
150         for size in all_sizes:
151             if size.id in size_kwargs:
152                 matching_sizes.append((size, size_kwargs[size.id]))
153             elif size.name in size_kwargs:
154                 matching_sizes.append((size, size_kwargs[size.name]))
155         return matching_sizes
156
157     def shutdown_windows(self):
158         return [int(n)
159                 for n in self.get('Cloud', 'shutdown_windows').split(',')]