Просмотр исходного кода

Amazon EC2 driver

This change adds an experimental AWS driver. It lacks some of the deeper
features of the openstack driver, such as quota management and image
building, but is highly functional for running tests on a static AMI.

Note that the test base had to be refactored to allow fixtures to be
customized in a more flexible way.

Change-Id: I313f9da435dfeb35591e37ad0bec921c8b5bc2b5
Co-Authored-By: Tristan Cacqueray <tdecacqu@redhat.com>
Co-Authored-By: David Moreau-Simard <dmsimard@redhat.com>
Co-AUthored-By: Clint Byrum <clint@fewbar.com>
tags/3.5.0
Tristan Cacqueray 1 год назад
Родитель
Сommit
aa16b8b891

+ 27
- 0
nodepool/driver/aws/__init__.py Просмотреть файл

@@ -0,0 +1,27 @@
1
+# Copyright 2018 Red Hat
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+#
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+from nodepool.driver import Driver
18
+from nodepool.driver.aws.config import AwsProviderConfig
19
+from nodepool.driver.aws.provider import AwsProvider
20
+
21
+
22
+class AwsDriver(Driver):
23
+    def getProviderConfig(self, provider):
24
+        return AwsProviderConfig(self, provider)
25
+
26
+    def getProvider(self, provider_config, use_taskmanager):
27
+        return AwsProvider(provider_config, use_taskmanager)

+ 285
- 0
nodepool/driver/aws/config.py Просмотреть файл

@@ -0,0 +1,285 @@
1
+# Copyright 2018 Red Hat
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+#
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+import math
18
+import voluptuous as v
19
+
20
+from nodepool.driver import ConfigPool
21
+from nodepool.driver import ConfigValue
22
+from nodepool.driver import ProviderConfig
23
+
24
+
25
+class ProviderCloudImage(ConfigValue):
26
+    def __init__(self):
27
+        self.name = None
28
+        self.image_id = None
29
+        self.image_name = None
30
+        self.username = None
31
+        self.connection_type = None
32
+        self.connection_port = None
33
+
34
+    def __eq__(self, other):
35
+        if isinstance(other, ProviderCloudImage):
36
+            return (self.name == other.name
37
+                    and self.image_id == other.image_id
38
+                    and self.image_name == other.image_name
39
+                    and self.username == other.username
40
+                    and self.connection_type == other.connection_type
41
+                    and self.connection_port == other.connection_port)
42
+        return False
43
+
44
+    def __repr__(self):
45
+        return "<ProviderCloudImage %s>" % self.name
46
+
47
+    @property
48
+    def external_name(self):
49
+        '''Human readable version of external.'''
50
+        return self.image_id or self.image_name or self.name
51
+
52
+
53
+class ProviderLabel(ConfigValue):
54
+    def __init__(self):
55
+        self.name = None
56
+        self.cloud_image = None
57
+        self.flavor_name = None
58
+        self.key_name = None
59
+        self.volume_size = None
60
+        self.volume_type = None
61
+        # The ProviderPool object that owns this label.
62
+        self.pool = None
63
+
64
+    def __eq__(self, other):
65
+        if isinstance(other, ProviderLabel):
66
+            # NOTE(Shrews): We intentionally do not compare 'pool' here
67
+            # since this causes recursive checks with ProviderPool.
68
+            return (other.name == self.name
69
+                    and other.cloud_image == self.cloud_image
70
+                    and other.flavor_name == self.flavor_name
71
+                    and other.key_name == self.key_name
72
+                    and other.volume_size == self.volume_size
73
+                    and other.volume_type == self.volume_type)
74
+        return False
75
+
76
+    def __repr__(self):
77
+        return "<ProviderLabel %s>" % self.name
78
+
79
+
80
+class ProviderPool(ConfigPool):
81
+    def __init__(self):
82
+        self.name = None
83
+        self.max_cores = None
84
+        self.max_ram = None
85
+        self.ignore_provider_quota = False
86
+        self.availability_zone = None
87
+        self.subnet_id = None
88
+        self.security_group_id = None
89
+        self.host_key_checking = True
90
+        self.labels = None
91
+        # The ProviderConfig object that owns this pool.
92
+        self.provider = None
93
+
94
+        # Initialize base class attributes
95
+        super().__init__()
96
+
97
+    def load(self, pool_config, full_config, provider):
98
+        super().load(pool_config)
99
+        self.name = pool_config['name']
100
+        self.provider = provider
101
+
102
+        self.max_cores = pool_config.get('max-cores', math.inf)
103
+        self.max_ram = pool_config.get('max-ram', math.inf)
104
+        self.ignore_provider_quota = pool_config.get(
105
+            'ignore-provider-quota', False)
106
+        self.availability_zone = pool_config.get('availability-zone')
107
+        self.security_group_id = pool_config.get('security-group-id')
108
+        self.subnet_id = pool_config.get('subnet-id')
109
+        self.host_key_checking = bool(
110
+            pool_config.get('host-key-checking', True))
111
+
112
+        for label in pool_config.get('labels', []):
113
+            pl = ProviderLabel()
114
+            pl.name = label['name']
115
+            pl.pool = self
116
+            self.labels[pl.name] = pl
117
+            cloud_image_name = label.get('cloud-image', None)
118
+            if cloud_image_name:
119
+                cloud_image = self.provider.cloud_images.get(
120
+                    cloud_image_name, None)
121
+                if not cloud_image:
122
+                    raise ValueError(
123
+                        "cloud-image %s does not exist in provider %s"
124
+                        " but is referenced in label %s" %
125
+                        (cloud_image_name, self.name, pl.name))
126
+            else:
127
+                cloud_image = None
128
+            pl.cloud_image = cloud_image
129
+            pl.flavor_name = label['flavor-name']
130
+            pl.key_name = label['key-name']
131
+            pl.volume_type = label.get('volume-type')
132
+            pl.volume_size = label.get('volume-size')
133
+            full_config.labels[label['name']].pools.append(self)
134
+
135
+    def __eq__(self, other):
136
+        if isinstance(other, ProviderPool):
137
+            # NOTE(Shrews): We intentionally do not compare 'provider' here
138
+            # since this causes recursive checks with OpenStackProviderConfig.
139
+            return (super().__eq__(other)
140
+                    and other.name == self.name
141
+                    and other.max_cores == self.max_cores
142
+                    and other.max_ram == self.max_ram
143
+                    and other.ignore_provider_quota == (
144
+                        self.ignore_provider_quota)
145
+                    and other.availability_zone == self.availability_zone
146
+                    and other.subnet_id == self.subnet_id
147
+                    and other.security_group_id == self.security_group_id
148
+                    and other.host_key_checking == self.host_key_checking
149
+                    and other.labels == self.labels)
150
+        return False
151
+
152
+    def __repr__(self):
153
+        return "<ProviderPool %s>" % self.name
154
+
155
+
156
+class AwsProviderConfig(ProviderConfig):
157
+    def __init__(self, driver, provider):
158
+        self.driver_object = driver
159
+        self.__pools = {}
160
+        self.profile_name = None
161
+        self.region_name = None
162
+        self.rate = None
163
+        self.boot_timeout = None
164
+        self.launch_retries = None
165
+        self.launch_timeout = None
166
+        self.cloud_images = {}
167
+        self.hostname_format = None
168
+        self.image_name_format = None
169
+        super().__init__(provider)
170
+
171
+    def __eq__(self, other):
172
+        if isinstance(other, AwsProviderConfig):
173
+            return (super().__eq__(other)
174
+                    and other.profile_name == self.profile_name
175
+                    and other.region_name == self.region_name
176
+                    and other.pools == self.pools
177
+                    and other.rate == self.rate
178
+                    and other.boot_timeout == self.boot_timeout
179
+                    and other.launch_retries == self.launch_retries
180
+                    and other.launch_timeout == self.launch_timeout
181
+                    and other.cloud_images == self.cloud_images)
182
+        return False
183
+
184
+    @property
185
+    def pools(self):
186
+        return self.__pools
187
+
188
+    @property
189
+    def manage_images(self):
190
+        return True
191
+
192
+    @staticmethod
193
+    def reset():
194
+        pass
195
+
196
+    def load(self, config):
197
+        self.profile_name = self.provider.get('profile-name')
198
+        self.region_name = self.provider.get('region-name')
199
+        self.rate = float(self.provider.get('rate', 1.0))
200
+        self.boot_timeout = self.provider.get('boot-timeout', 60)
201
+        self.launch_retries = self.provider.get('launch-retries', 3)
202
+        self.launch_timeout = self.provider.get('launch-timeout', 3600)
203
+        self.hostname_format = self.provider.get(
204
+            'hostname-format',
205
+            '{label.name}-{provider.name}-{node.id}'
206
+        )
207
+        self.image_name_format = self.provider.get(
208
+            'image-name-format',
209
+            '{image_name}-{timestamp}'
210
+        )
211
+
212
+        default_port_mapping = {
213
+            'ssh': 22,
214
+            'winrm': 5986,
215
+        }
216
+        # TODO: diskimages
217
+
218
+        for image in self.provider.get('cloud-images', []):
219
+            i = ProviderCloudImage()
220
+            i.name = image['name']
221
+            i.image_id = image.get('image-id', None)
222
+            i.image_name = image.get('image-name', None)
223
+            i.username = image.get('username', None)
224
+            i.connection_type = image.get('connection-type', 'ssh')
225
+            i.connection_port = image.get(
226
+                'connection-port',
227
+                default_port_mapping.get(i.connection_type, 22))
228
+            self.cloud_images[i.name] = i
229
+
230
+        for pool in self.provider.get('pools', []):
231
+            pp = ProviderPool()
232
+            pp.load(pool, config, self)
233
+            self.pools[pp.name] = pp
234
+
235
+    def getSchema(self):
236
+        pool_label = {
237
+            v.Required('name'): str,
238
+            v.Exclusive('cloud-image', 'label-image'): str,
239
+            v.Required('flavor-name'): str,
240
+            v.Required('key-name'): str,
241
+            'volume-type': str,
242
+            'volume-size': int
243
+        }
244
+
245
+        pool = ConfigPool.getCommonSchemaDict()
246
+        pool.update({
247
+            v.Required('name'): str,
248
+            v.Required('labels'): [pool_label],
249
+            'max-cores': int,
250
+            'max-ram': int,
251
+            'availability-zone': str,
252
+            'security-group-id': str,
253
+            'subnet-id': str,
254
+        })
255
+
256
+        provider_cloud_images = {
257
+            'name': str,
258
+            'connection-type': str,
259
+            'connection-port': int,
260
+            v.Exclusive('image-id', 'cloud-image-name-or-id'): str,
261
+            v.Exclusive('image-name', 'cloud-image-name-or-id'): str,
262
+            'username': str,
263
+        }
264
+
265
+        provider = ProviderConfig.getCommonSchemaDict()
266
+        provider.update({
267
+            v.Required('pools'): [pool],
268
+            v.Required('region-name'): str,
269
+            'profile-name': str,
270
+            'cloud-images': [provider_cloud_images],
271
+            'rate': v.Coerce(float),
272
+            'hostname-format': str,
273
+            'image-name-format': str,
274
+            'boot-timeout': int,
275
+            'launch-timeout': int,
276
+            'launch-retries': int,
277
+        })
278
+        return v.Schema(provider)
279
+
280
+    def getSupportedLabels(self, pool_name=None):
281
+        labels = set()
282
+        for pool in self.pools.values():
283
+            if not pool_name or (pool.name == pool_name):
284
+                labels.update(pool.labels.keys())
285
+        return labels

+ 157
- 0
nodepool/driver/aws/handler.py Просмотреть файл

@@ -0,0 +1,157 @@
1
+# Copyright 2018 Red Hat
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import logging
16
+import time
17
+
18
+from nodepool import exceptions
19
+from nodepool import zk
20
+from nodepool.driver.utils import NodeLauncher
21
+from nodepool.driver import NodeRequestHandler
22
+from nodepool.nodeutils import nodescan
23
+
24
+
25
+class AwsInstanceLauncher(NodeLauncher):
26
+    def __init__(self, handler, node, provider_config, provider_label):
27
+        super().__init__(handler.zk, node, provider_config)
28
+        self.retries = provider_config.launch_retries
29
+        self.pool = provider_config.pools[provider_label.pool.name]
30
+        self.handler = handler
31
+        self.zk = handler.zk
32
+        self.boot_timeout = provider_config.boot_timeout
33
+        self.label = provider_label
34
+
35
+    def launch(self):
36
+        self.log.debug("Starting %s instance" % self.node.type)
37
+        attempts = 1
38
+        while attempts <= self.retries:
39
+            try:
40
+                instance = self.handler.manager.createInstance(self.label)
41
+                break
42
+            except Exception:
43
+                if attempts <= self.retries:
44
+                    self.log.exception(
45
+                        "Launch attempt %d/%d failed for node %s:",
46
+                        attempts, self.retries, self.node.id)
47
+                if attempts == self.retries:
48
+                    raise
49
+                attempts += 1
50
+            time.sleep(1)
51
+
52
+        instance.create_tags(Tags=[{'Key': 'nodepool_id',
53
+                                    'Value': str(self.node.id)}])
54
+        instance_id = instance.id
55
+        self.node.external_id = instance_id
56
+        self.zk.storeNode(self.node)
57
+
58
+        boot_start = time.monotonic()
59
+        while time.monotonic() - boot_start < self.boot_timeout:
60
+            state = instance.state.get('Name')
61
+            self.log.debug("Instance %s is %s" % (instance_id, state))
62
+            if state == 'running':
63
+                break
64
+            time.sleep(0.5)
65
+            instance.reload()
66
+        if state != 'running':
67
+            raise exceptions.LaunchStatusException(
68
+                "Instance %s failed to start: %s" % (instance_id, state))
69
+
70
+        server_ip = instance.public_ip_address
71
+        if not server_ip:
72
+            raise exceptions.LaunchStatusException(
73
+                "Instance %s doesn't have a public ip" % instance_id)
74
+
75
+        self.node.connection_port = self.label.cloud_image.connection_port
76
+        self.node.connection_type = self.label.cloud_image.connection_type
77
+        if self.pool.host_key_checking:
78
+            try:
79
+                if self.node.connection_type == 'ssh':
80
+                    gather_hostkeys = True
81
+                else:
82
+                    gather_hostkeys = False
83
+                keys = nodescan(server_ip, port=self.node.connection_port,
84
+                                timeout=180, gather_hostkeys=gather_hostkeys)
85
+            except Exception:
86
+                raise exceptions.LaunchKeyscanException(
87
+                    "Can't scan instance %s key" % instance_id)
88
+
89
+        self.log.info("Instance %s ready" % instance_id)
90
+        self.node.state = zk.READY
91
+        self.node.external_id = instance_id
92
+        self.node.hostname = server_ip
93
+        self.node.interface_ip = server_ip
94
+        self.node.public_ipv4 = server_ip
95
+        self.node.host_keys = keys
96
+        self.node.username = self.label.cloud_image.username
97
+        self.zk.storeNode(self.node)
98
+        self.log.info("Instance %s is ready", instance_id)
99
+
100
+
101
+class AwsNodeRequestHandler(NodeRequestHandler):
102
+    log = logging.getLogger("nodepool.driver.aws."
103
+                            "AwsNodeRequestHandler")
104
+
105
+    def __init__(self, pw, request):
106
+        super().__init__(pw, request)
107
+        self._threads = []
108
+
109
+    @property
110
+    def alive_thread_count(self):
111
+        count = 0
112
+        for t in self._threads:
113
+            if t.isAlive():
114
+                count += 1
115
+        return count
116
+
117
+    def imagesAvailable(self):
118
+        '''
119
+        Determines if the requested images are available for this provider.
120
+
121
+        :returns: True if it is available, False otherwise.
122
+        '''
123
+        if self.provider.manage_images:
124
+            for label in self.request.node_types:
125
+                if self.pool.labels[label].cloud_image:
126
+                    if not self.manager.labelReady(self.pool.labels[label]):
127
+                        return False
128
+        return True
129
+
130
+    def launchesComplete(self):
131
+        '''
132
+        Check if all launch requests have completed.
133
+
134
+        When all of the Node objects have reached a final state (READY or
135
+        FAILED), we'll know all threads have finished the launch process.
136
+        '''
137
+        if not self._threads:
138
+            return True
139
+
140
+        # Give the NodeLaunch threads time to finish.
141
+        if self.alive_thread_count:
142
+            return False
143
+
144
+        node_states = [node.state for node in self.nodeset]
145
+
146
+        # NOTE: It very important that NodeLauncher always sets one of
147
+        # these states, no matter what.
148
+        if not all(s in (zk.READY, zk.FAILED) for s in node_states):
149
+            return False
150
+
151
+        return True
152
+
153
+    def launch(self, node):
154
+        label = self.pool.labels[node.type[0]]
155
+        thd = AwsInstanceLauncher(self, node, self.provider, label)
156
+        thd.start()
157
+        self._threads.append(thd)

+ 151
- 0
nodepool/driver/aws/provider.py Просмотреть файл

@@ -0,0 +1,151 @@
1
+# Copyright 2018 Red Hat
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import logging
16
+import boto3
17
+
18
+from nodepool.driver import Provider
19
+from nodepool.driver.aws.handler import AwsNodeRequestHandler
20
+
21
+
22
+class AwsInstance:
23
+    def __init__(self, name, metadatas, provider):
24
+        self.id = name
25
+        self.name = name
26
+        self.metadata = {}
27
+        if metadatas:
28
+            for metadata in metadatas:
29
+                if metadata["Key"] == "nodepool_id":
30
+                    self.metadata = {
31
+                        'nodepool_provider_name': provider.name,
32
+                        'nodepool_node_id': metadata["Value"],
33
+                    }
34
+                    break
35
+
36
+    def get(self, name, default=None):
37
+        return getattr(self, name, default)
38
+
39
+
40
+class AwsProvider(Provider):
41
+    log = logging.getLogger("nodepool.driver.aws.AwsProvider")
42
+
43
+    def __init__(self, provider, *args):
44
+        self.provider = provider
45
+        self.ec2 = None
46
+
47
+    def getRequestHandler(self, poolworker, request):
48
+        return AwsNodeRequestHandler(poolworker, request)
49
+
50
+    def start(self, zk_conn):
51
+        if self.ec2 is not None:
52
+            return True
53
+        self.log.debug("Starting")
54
+        self.aws = boto3.Session(
55
+            region_name=self.provider.region_name,
56
+            profile_name=self.provider.profile_name)
57
+        self.ec2 = self.aws.resource('ec2')
58
+
59
+    def stop(self):
60
+        self.log.debug("Stopping")
61
+
62
+    def listNodes(self):
63
+        servers = []
64
+
65
+        for instance in self.ec2.instances.all():
66
+            if instance.state["Name"].lower() == "terminated":
67
+                continue
68
+            servers.append(AwsInstance(
69
+                instance.id, instance.tags, self.provider))
70
+        return servers
71
+
72
+    def getImage(self, image_id):
73
+        return self.ec2.Image(image_id)
74
+
75
+    def labelReady(self, label):
76
+        if not label.cloud_image:
77
+            msg = "A cloud-image (AMI) must be supplied with the AWS driver."
78
+            raise Exception(msg)
79
+
80
+        image = self.getImage(label.cloud_image.external_name)
81
+        # Image loading is deferred, check if it's really there
82
+        if image.state != 'available':
83
+            self.log.warning(
84
+                "Provider %s is configured to use %s as the AMI for"
85
+                " label %s and that AMI is there but unavailable in the"
86
+                " cloud." % (self.provider.name,
87
+                             label.cloud_image.external_name,
88
+                             label.name))
89
+            return False
90
+        return True
91
+
92
+    def join(self):
93
+        return True
94
+
95
+    def cleanupLeakedResources(self):
96
+        # TODO: remove leaked resources if any
97
+        pass
98
+
99
+    def cleanupNode(self, server_id):
100
+        if self.ec2 is None:
101
+            return False
102
+        instance = self.ec2.Instance(server_id)
103
+        instance.terminate()
104
+
105
+    def waitForNodeCleanup(self, server_id):
106
+        # TODO: track instance deletion
107
+        return True
108
+
109
+    def createInstance(self, label):
110
+        image_name = label.cloud_image.external_name
111
+        args = dict(
112
+            ImageId=image_name,
113
+            MinCount=1,
114
+            MaxCount=1,
115
+            KeyName=label.key_name,
116
+            InstanceType=label.flavor_name,
117
+            NetworkInterfaces=[{
118
+                'AssociatePublicIpAddress': True,
119
+                'DeviceIndex': 0}])
120
+
121
+        if label.pool.security_group_id:
122
+            args['NetworkInterfaces'][0]['Groups'] = [
123
+                label.pool.security_group_id
124
+            ]
125
+        if label.pool.subnet_id:
126
+            args['NetworkInterfaces'][0]['SubnetId'] = label.pool.subnet_id
127
+
128
+        # Default block device mapping parameters are embedded in AMIs.
129
+        # We might need to supply our own mapping before lauching the instance.
130
+        # We basically want to make sure DeleteOnTermination is true and be
131
+        # able to set the volume type and size.
132
+        image = self.getImage(image_name)
133
+        # TODO: Flavors can also influence whether or not the VM spawns with a
134
+        # volume -- we basically need to ensure DeleteOnTermination is true
135
+        if hasattr(image, 'block_device_mappings'):
136
+            bdm = image.block_device_mappings
137
+            mapping = bdm[0]
138
+            if 'Ebs' in mapping:
139
+                mapping['Ebs']['DeleteOnTermination'] = True
140
+                if label.volume_size:
141
+                    mapping['Ebs']['VolumeSize'] = label.volume_size
142
+                if label.volume_type:
143
+                    mapping['Ebs']['VolumeType'] = label.volume_type
144
+                # If the AMI is a snapshot, we cannot supply an "encrypted"
145
+                # parameter
146
+                if 'Encrypted' in mapping['Ebs']:
147
+                    del mapping['Ebs']['Encrypted']
148
+                args['BlockDeviceMappings'] = [mapping]
149
+
150
+        instances = self.ec2.create_instances(**args)
151
+        return self.ec2.Instance(instances[0].id)

+ 16
- 13
nodepool/tests/__init__.py Просмотреть файл

@@ -334,19 +334,22 @@ class DBTestCase(BaseTestCase):
334 334
             self.useFixture(images_dir)
335 335
         build_log_dir = fixtures.TempDir()
336 336
         self.useFixture(build_log_dir)
337
-        configfile = os.path.join(os.path.dirname(__file__),
338
-                                  'fixtures', filename)
339
-        (fd, path) = tempfile.mkstemp()
340
-        with open(configfile, 'rb') as conf_fd:
341
-            config = conf_fd.read().decode('utf8')
342
-            data = config.format(images_dir=images_dir.path,
343
-                                 build_log_dir=build_log_dir.path,
344
-                                 context_name=context_name,
345
-                                 zookeeper_host=self.zookeeper_host,
346
-                                 zookeeper_port=self.zookeeper_port,
347
-                                 zookeeper_chroot=self.zookeeper_chroot)
348
-            os.write(fd, data.encode('utf8'))
349
-        os.close(fd)
337
+        if filename.startswith('/'):
338
+            path = filename
339
+        else:
340
+            configfile = os.path.join(os.path.dirname(__file__),
341
+                                      'fixtures', filename)
342
+            (fd, path) = tempfile.mkstemp()
343
+            with open(configfile, 'rb') as conf_fd:
344
+                config = conf_fd.read().decode('utf8')
345
+                data = config.format(images_dir=images_dir.path,
346
+                                     build_log_dir=build_log_dir.path,
347
+                                     context_name=context_name,
348
+                                     zookeeper_host=self.zookeeper_host,
349
+                                     zookeeper_port=self.zookeeper_port,
350
+                                     zookeeper_chroot=self.zookeeper_chroot)
351
+                os.write(fd, data.encode('utf8'))
352
+            os.close(fd)
350 353
         self._config_images_dir = images_dir
351 354
         self._config_build_log_dir = build_log_dir
352 355
         validator = ConfigValidator(path)

+ 26
- 0
nodepool/tests/fixtures/aws.yaml Просмотреть файл

@@ -0,0 +1,26 @@
1
+zookeeper-servers:
2
+  - host: null
3
+    port: null
4
+    chroot: null
5
+
6
+labels:
7
+  - name: ubuntu1404
8
+
9
+providers:
10
+  - name: ec2-us-west-2
11
+    driver: aws
12
+    region-name: us-west-2
13
+    cloud-images:
14
+      - name: ubuntu1404
15
+        image-id: ami-1e749f67
16
+        username: ubuntu
17
+    pools:
18
+      - name: main
19
+        max-servers: 5
20
+        subnet-id: null
21
+        security-group-id: null
22
+        labels:
23
+          - name: ubuntu1404
24
+            cloud-image: ubuntu1404
25
+            flavor-name: t3.medium
26
+            key-name: zuul

+ 22
- 0
nodepool/tests/fixtures/config_validate/good.yaml Просмотреть файл

@@ -23,6 +23,7 @@ labels:
23 23
   - name: pod-fedora
24 24
   - name: openshift-project
25 25
   - name: openshift-pod
26
+  - name: centos-ami
26 27
 
27 28
 providers:
28 29
   - name: cloud1
@@ -132,6 +133,27 @@ providers:
132 133
             memory: 512
133 134
             cpu: 2
134 135
 
136
+  - name: ec2-us-east-2
137
+    driver: aws
138
+    region-name: us-east-2
139
+    profile-name: default
140
+    cloud-images:
141
+      - name: centos-ami
142
+        image-id: ami-cfdafaaa
143
+        username: centos
144
+    pools:
145
+      - name: main
146
+        max-servers: 42
147
+        security-group-id: sg-8bfe86352e334a80a
148
+        subnet-id: subnet-bb3605b5f0fa40e1b
149
+        labels:
150
+          - name: centos-ami
151
+            cloud-image: centos-ami
152
+            flavor-name: t2.micro
153
+            key-name: zuul
154
+            volume-type: gp2
155
+            volume-size: 80
156
+
135 157
 diskimages:
136 158
   - name: trusty
137 159
     formats:

+ 95
- 0
nodepool/tests/unit/test_driver_aws.py Просмотреть файл

@@ -0,0 +1,95 @@
1
+# Copyright (C) 2018 Red Hat
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+# See the License for the specific language governing permissions and
14
+# limitations under the License.
15
+
16
+import fixtures
17
+import logging
18
+import os
19
+import tempfile
20
+from unittest.mock import patch
21
+
22
+import boto3
23
+from moto import mock_ec2
24
+import yaml
25
+
26
+from nodepool import tests
27
+from nodepool import zk
28
+
29
+
30
+class TestDriverAws(tests.DBTestCase):
31
+    log = logging.getLogger("nodepool.TestDriverAws")
32
+
33
+    @mock_ec2
34
+    def test_ec2_machine(self):
35
+        aws_id = 'AK000000000000000000'
36
+        aws_key = '0123456789abcdef0123456789abcdef0123456789abcdef'
37
+        self.useFixture(
38
+            fixtures.EnvironmentVariable('AWS_ACCESS_KEY_ID', aws_id))
39
+        self.useFixture(
40
+            fixtures.EnvironmentVariable('AWS_SECRET_ACCESS_KEY', aws_key))
41
+
42
+        ec2 = boto3.client('ec2', region_name='us-west-2')
43
+
44
+        # TEST-NET-3
45
+        vpc = ec2.create_vpc(CidrBlock='203.0.113.0/24')
46
+
47
+        subnet = ec2.create_subnet(
48
+            CidrBlock='203.0.113.128/25', VpcId=vpc['Vpc']['VpcId'])
49
+        subnet_id = subnet['Subnet']['SubnetId']
50
+        sg = ec2.create_security_group(
51
+            GroupName='zuul-nodes', VpcId=vpc['Vpc']['VpcId'],
52
+            Description='Zuul Nodes')
53
+        sg_id = sg['GroupId']
54
+
55
+        ec2_template = os.path.join(
56
+            os.path.dirname(__file__), '..', 'fixtures', 'aws.yaml')
57
+        raw_config = yaml.safe_load(open(ec2_template))
58
+        raw_config['zookeeper-servers'][0] = {
59
+            'host': self.zookeeper_host,
60
+            'port': self.zookeeper_port,
61
+            'chroot': self.zookeeper_chroot,
62
+        }
63
+        raw_config['providers'][0]['pools'][0]['subnet-id'] = subnet_id
64
+        raw_config['providers'][0]['pools'][0]['security-group-id'] = sg_id
65
+        with tempfile.NamedTemporaryFile() as tf:
66
+            tf.write(yaml.safe_dump(
67
+                raw_config, default_flow_style=False).encode('utf-8'))
68
+            tf.flush()
69
+            configfile = self.setup_config(tf.name)
70
+            pool = self.useNodepool(configfile, watermark_sleep=1)
71
+            pool.start()
72
+            req = zk.NodeRequest()
73
+            req.state = zk.REQUESTED
74
+            req.node_types.append('ubuntu1404')
75
+            with patch('nodepool.driver.aws.handler.nodescan') as nodescan:
76
+                nodescan.return_value = 'MOCK KEY'
77
+                self.zk.storeNodeRequest(req)
78
+
79
+                self.log.debug("Waiting for request %s", req.id)
80
+                req = self.waitForNodeRequest(req)
81
+            self.assertEqual(req.state, zk.FULFILLED)
82
+
83
+            self.assertNotEqual(req.nodes, [])
84
+            node = self.zk.getNode(req.nodes[0])
85
+            self.assertEqual(node.allocated_to, req.id)
86
+            self.assertEqual(node.state, zk.READY)
87
+            self.assertIsNotNone(node.launcher)
88
+            self.assertEqual(node.connection_type, 'ssh')
89
+            nodescan.assert_called_with(
90
+                node.interface_ip, port=22, timeout=180, gather_hostkeys=True)
91
+
92
+            node.state = zk.DELETING
93
+            self.zk.storeNode(node)
94
+
95
+            self.waitForNodeDeletion(node)

+ 7
- 0
releasenotes/notes/aws-driver-6d6c25381066b9ca.yaml Просмотреть файл

@@ -0,0 +1,7 @@
1
+---
2
+prelude: Amazon Web Services (AWS) EC2 Driver
3
+features:
4
+  - The new Amazon Web Services (AWS) EC2 Driver allows launching EC2 instances as nodes.
5
+issues:
6
+  - The AWS driver does not support quota management at this time.
7
+  - The AWS driver does not support custom image building.

+ 1
- 0
requirements.txt Просмотреть файл

@@ -15,3 +15,4 @@ kazoo
15 15
 Paste
16 16
 WebOb>=1.8.1
17 17
 openshift
18
+boto3

+ 1
- 0
test-requirements.txt Просмотреть файл

@@ -7,3 +7,4 @@ python-subunit
7 7
 stestr>=1.0.0 # Apache-2.0
8 8
 testscenarios
9 9
 testtools>=0.9.27
10
+moto

Загрузка…
Отмена
Сохранить