Merge lp:~ted/snapcraft/aws-iot into lp:~snappy-dev/snapcraft/core

Proposed by Ted Gould
Status: Work in progress
Proposed branch: lp:~ted/snapcraft/aws-iot
Merge into: lp:~snappy-dev/snapcraft/core
Diff against target: 545 lines (+373/-21)
8 files modified
snapcraft/cmds.py (+1/-4)
snapcraft/lifecycle.py (+5/-5)
snapcraft/plugins/awscli.py (+105/-0)
snapcraft/plugins/awsiot.py (+227/-0)
snapcraft/plugins/python2.py (+14/-1)
snapcraft/plugins/python3.py (+14/-1)
snapcraft/tests/test_cmds.py (+6/-9)
snapcraft/tests/test_lifecycle.py (+1/-1)
To merge this branch: bzr merge lp:~ted/snapcraft/aws-iot
Reviewer Review Type Date Requested Status
Sergio Schvezov Needs Fixing
Review via email: mp+276064@code.launchpad.net

Commit message

Add plugins for working with AWS' IoT features

To post a comment you must log in.
lp:~ted/snapcraft/aws-iot updated
252. By Ted Gould

Fix function call

Revision history for this message
Sergio Schvezov (sergiusens) :
Revision history for this message
Sergio Schvezov (sergiusens) wrote :

Looks good, I don't follow, can I see an example for this? I guess that adding the docstring like I ask for can clarify lots here.

review: Needs Fixing
lp:~ted/snapcraft/aws-iot updated
253. By Ted Gould

Testing fixes from Sergio

254. By Ted Gould

Adding docstrings

255. By Ted Gould

Making sure we get more files

Revision history for this message
Sergio Schvezov (sergiusens) wrote :

This looks really good but the need for AWSCLIPlugin I want to avoid.

I still strongly believe that the AWSCLIPlugin should not be needed and a build-package for awscli should be used (we can update the deb in our ppa so we have the IOT option if need be).

Or is there a reason for the aws cli to live in the snap?

There are also some inline comments.

ftr,
$ rmadison awscli
 awscli | 1.2.9-2 | trusty/universe | source, all
 awscli | 1.7.0-1 | vivid/universe | source, all
 awscli | 1.7.0-1build1 | wily/universe | source, all
 awscli | 1.7.0-1build1 | xenial/universe | source, all

review: Needs Fixing
Revision history for this message
Sergio Schvezov (sergiusens) :
Revision history for this message
Sergio Schvezov (sergiusens) :
lp:~ted/snapcraft/aws-iot updated
256. By Ted Gould

Removing unneeded None

Revision history for this message
Ted Gould (ted) wrote :

On Thu, 2015-10-29 at 11:48 +0000, Sergio Schvezov wrote:
> > + def _keys_from_local(self, certsdir):
> > + certsjson = None
>
> this shouldn't be needed.
Python is weird. r256
> > === modified file 'snapcraft/plugins/python3.py'
> > --- snapcraft/plugins/python3.py 2015-10-27 19:23:02 +0000
> > +++ snapcraft/plugins/python3.py 2015-10-29 04:34:48 +0000
> > @@ -48,6 +48,15 @@
> > schema['properties']['requirements'] = {
> > 'type': 'string',
> > }
> > + schema['properties']['pip-packages'] = {
> >

>
>
> doesn't this fix the other bug about list of requirements?
>

No, because there are apparently some projects (plainbox) that use
multiple files of requirements. So there's another feature there.

Unmerged revisions

256. By Ted Gould

Removing unneeded None

255. By Ted Gould

Making sure we get more files

254. By Ted Gould

Adding docstrings

253. By Ted Gould

Testing fixes from Sergio

252. By Ted Gould

Fix function call

251. By Ted Gould

Adding the plugins to the command tests

250. By Ted Gould

Switching to the 'new run'

249. By Ted Gould

Merge trunk

248. By Ted Gould

Reduce complexity of cert gen

247. By Ted Gould

Include cleanups

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'snapcraft/cmds.py'
2--- snapcraft/cmds.py 2015-10-26 16:35:16 +0000
3+++ snapcraft/cmds.py 2015-10-29 14:24:50 +0000
4@@ -273,10 +273,7 @@
5 parts_files = {}
6 for part in parts:
7 # Gather our own files up
8- fileset = getattr(part.code.options, 'stage', ['*']) or ['*']
9- part_files, _ = lifecycle.migratable_filesets(
10- fileset,
11- part.installdir)
12+ part_files, _ = part.migratable_fileset_for('stage')
13
14 # Scan previous parts for collisions
15 for other_part_name in parts_files:
16
17=== modified file 'snapcraft/lifecycle.py'
18--- snapcraft/lifecycle.py 2015-10-26 14:04:47 +0000
19+++ snapcraft/lifecycle.py 2015-10-29 14:24:50 +0000
20@@ -151,11 +151,11 @@
21 self.code.build()
22 self.mark_done('build')
23
24- def _migratable_fileset_for(self, stage):
25+ def migratable_fileset_for(self, stage):
26 plugin_fileset = self.code.snap_fileset()
27 fileset = getattr(self.code.options, stage, ['*']) or ['*']
28 fileset.extend(plugin_fileset)
29- return migratable_filesets(fileset, self.installdir)
30+ return _migratable_filesets(fileset, self.installdir)
31
32 def _organize(self):
33 organize_fileset = getattr(self.code.options, 'organize', {}) or {}
34@@ -185,7 +185,7 @@
35
36 self.notify_stage("Staging")
37 self._organize()
38- snap_files, snap_dirs = self._migratable_fileset_for('stage')
39+ snap_files, snap_dirs = self.migratable_fileset_for('stage')
40
41 try:
42 _migrate_files(snap_files, snap_dirs, self.installdir,
43@@ -205,7 +205,7 @@
44 self.makedirs()
45
46 self.notify_stage("Snapping")
47- snap_files, snap_dirs = self._migratable_fileset_for('snap')
48+ snap_files, snap_dirs = self.migratable_fileset_for('snap')
49
50 try:
51 _migrate_files(snap_files, snap_dirs, self.stagedir, self.snapdir)
52@@ -274,7 +274,7 @@
53 return PluginHandler(plugin_name, part_name, properties)
54
55
56-def migratable_filesets(fileset, srcdir):
57+def _migratable_filesets(fileset, srcdir):
58 includes, excludes = _get_file_list(fileset)
59
60 include_files = _generate_include_set(srcdir, includes)
61
62=== added file 'snapcraft/plugins/awscli.py'
63--- snapcraft/plugins/awscli.py 1970-01-01 00:00:00 +0000
64+++ snapcraft/plugins/awscli.py 2015-10-29 14:24:50 +0000
65@@ -0,0 +1,105 @@
66+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
67+#
68+# Copyright (C) 2015 Canonical Ltd.
69+#
70+# This program is free software: you can redistribute it and/or modify
71+# it under the terms of the GNU General Public License version 3 as
72+# published by the Free Software Foundation.
73+#
74+# This program is distributed in the hope that it will be useful,
75+# but WITHOUT ANY WARRANTY; without even the implied warranty of
76+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
77+# GNU General Public License for more details.
78+#
79+# You should have received a copy of the GNU General Public License
80+# along with this program. If not, see <http://www.gnu.org/licenses/>.
81+
82+"""This plugin brings in the AWS command line interface and configures it.
83+
84+The AWS command line can be used for a variety of things including accessing
85+s3 or setting up ec2 services. It also is used for setting IoT things. This
86+plugin takes configuration values for your AWS keys, downloads and installs
87+the client into your snap, and configures it with those keys.
88+
89+Settings for this plugin include:
90+
91+ - accesskeyid:
92+ (string)
93+ AWS Access Key to use
94+ - secreetaccesskey:
95+ (string)
96+ AWS Secret Key
97+ - region
98+ (string)
99+ Region of EC2 to use, defaults to us-east-1
100+
101+It is important to not that the key will be stored in the snap itself and
102+could be harvested with appropriate tools.
103+"""
104+
105+import os
106+import os.path
107+import snapcraft.plugins.python3
108+
109+
110+class AWSCLIPlugin(snapcraft.plugins.python3.Python3Plugin):
111+
112+ @classmethod
113+ def schema(cls):
114+ schema = super().schema()
115+ schema['properties']['accesskeyid'] = {
116+ 'type': 'string',
117+ 'default': ''
118+ }
119+ schema['properties']['secretaccesskey'] = {
120+ 'type': 'string',
121+ 'default': ''
122+ }
123+ schema['properties']['region'] = {
124+ 'type': 'string',
125+ 'default': 'us-east-1'
126+ }
127+
128+ schema['required'] = ['accesskeyid', 'secretaccesskey']
129+
130+ return schema
131+
132+ def __init__(self, name, options):
133+ options.source = "https://github.com/aws/aws-cli.git"
134+ options.source_type = 'git'
135+ super().__init__(name, options)
136+
137+ def build(self):
138+ super().build()
139+
140+ aws = ['python3', os.path.join(self.installdir, 'usr', 'bin', 'aws')]
141+
142+ self.run(aws + ['configure',
143+ 'set', 'region', self.options.region])
144+ self.run(aws + ['configure',
145+ 'set', 'aws_access_key_id', self.options.accesskeyid])
146+ self.run(aws + ['configure',
147+ 'set', 'aws_secret_access_key',
148+ self.options.secretaccesskey])
149+
150+ def env(self, root):
151+ env = super().env(root)
152+ env.extend(['AWS_ACCESS_KEY_ID=%s' % self.options.accesskeyid,
153+ 'AWS_SECRET_ACCESS_KEY=%s' % self.options.secretaccesskey])
154+ return env
155+
156+ def snap_fileset(self):
157+ fileset = super().snap_fileset()
158+ fileset.append('-usr/bin/pip*')
159+ fileset.append('-usr/lib/python3/dist-packages/easy-install.pth')
160+ fileset.append('-usr/lib/python*/__pycache__/*.pyc')
161+ fileset.append('-usr/lib/python*/*/__pycache__/*.pyc')
162+ fileset.append('-usr/lib/python*/*/*/__pycache__/*.pyc')
163+ fileset.append('-usr/lib/python*/*/*/*/__pycache__/*.pyc')
164+ fileset.append('-usr/lib/python*/*/*/*/*/__pycache__/*.pyc')
165+ fileset.append('-usr/lib/python*/*/*/*/*/*/__pycache__/*.pyc')
166+ fileset.append('-usr/lib/python*/*/*/*/*/*/*/__pycache__/*.pyc')
167+ fileset.append('-usr/lib/python*/*/*/*/*/*/*/*/__pycache__/*.pyc')
168+ fileset.append('-usr/lib/python*/*/*/*/*/*/*/*/*/__pycache__/*.pyc')
169+ fileset.append('-usr/lib/python*/*/*/*/*/*/*/*/*/*/__pycache__/*.pyc')
170+ return fileset
171
172=== added file 'snapcraft/plugins/awsiot.py'
173--- snapcraft/plugins/awsiot.py 1970-01-01 00:00:00 +0000
174+++ snapcraft/plugins/awsiot.py 2015-10-29 14:24:50 +0000
175@@ -0,0 +1,227 @@
176+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
177+#
178+# Copyright (C) 2015 Canonical Ltd.
179+#
180+# This program is free software: you can redistribute it and/or modify
181+# it under the terms of the GNU General Public License version 3 as
182+# published by the Free Software Foundation.
183+#
184+# This program is distributed in the hope that it will be useful,
185+# but WITHOUT ANY WARRANTY; without even the implied warranty of
186+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
187+# GNU General Public License for more details.
188+#
189+# You should have received a copy of the GNU General Public License
190+# along with this program. If not, see <http://www.gnu.org/licenses/>.
191+
192+"""A plugin to configure an AWS IoT thing and setup for MQTT certs
193+
194+The AWS IoT feature requires registration and configuration to communicate
195+with the EC2 cloud. This plugin has configuration options for many of
196+these settings, and some it can generate if there isn't an appropriate
197+value to get. It will provide a basis for any software designed to
198+communicate with the EC2 IoT interfaces.
199+
200+ - generatekeys
201+ (string)
202+ True if new keys should be generated by Amazon,
203+ otherwise generate keys locally
204+ - policydocument
205+ (string)
206+ Which policy document should be used. Optional.
207+ A doc which allows all IoT will be used if not specified
208+ - policyname
209+ (string)
210+ Which policy name should be used. Optional.
211+ 'PubSubToAnyTopic' will be used if not specified
212+ - thing
213+ (string)
214+ The thing to create
215+ - endpoint
216+ (string)
217+ AWS Endpoint to use if non-default
218+
219+"""
220+
221+import os
222+import snapcraft
223+import urllib.request
224+import os.path
225+import json
226+import subprocess
227+
228+
229+class AWSIoTPlugin(snapcraft.BasePlugin):
230+
231+ @classmethod
232+ def schema(cls):
233+ return {
234+ '$schema': 'http://json-schema.org/draft-04/schema#',
235+ 'type': 'object',
236+ 'properties': {
237+ 'generatekeys': {
238+ 'type': 'boolean',
239+ 'default': True
240+ },
241+ 'policydocument': {
242+ 'type': 'string',
243+ 'default': ''
244+ },
245+ 'policyname': {
246+ 'type': 'string',
247+ 'default': 'PubSubToAnyTopic'
248+ },
249+ 'thing': {
250+ 'type': 'string',
251+ },
252+ 'endpoint': {
253+ 'type': 'string',
254+ },
255+ },
256+ 'required': ['thing']
257+ }
258+
259+ def __init__(self, name, options):
260+ super().__init__(name, options)
261+ self.aws = ['python3',
262+ os.path.join(self.stagedir, 'usr', 'bin', 'aws'),
263+ 'iot']
264+
265+ if (options.endpoint):
266+ self.aws.extend(['--endpoint', options.endpoint])
267+
268+ def pull(self):
269+ return True
270+
271+ def run_to_file(self, cmds, filename):
272+ output = self.run_output(cmds, cwd=self.builddir)
273+ with open(filename, 'w') as f:
274+ f.write(output)
275+
276+ def _keys_from_aws(self, certsdir):
277+ # generate new keys
278+ self.run_to_file(self.aws + ['create-keys-and-certificate',
279+ '--set-as-active'],
280+ os.path.join(certsdir, 'certs.json'))
281+ # separate into different files
282+ with open(os.path.join(certsdir, 'certs.json')) as data_file:
283+ certsjson = json.load(data_file)
284+
285+ with open(os.path.join(certsdir, 'cert.pem'), 'w') as text_file:
286+ text_file.write(self.data['certificatePem'])
287+ with open(os.path.join(certsdir, 'privateKey.pem'), 'w') \
288+ as text_file:
289+ text_file.write(self.data['keyPair']['PrivateKey'])
290+ with open(os.path.join(certsdir, 'publicKey.pem'), 'w') \
291+ as text_file:
292+ text_file.write(self.data['keyPair']['PublicKey'])
293+
294+ return certsjson
295+
296+ def _keys_from_local(self, certsdir):
297+ certsjson = None
298+
299+ # generate private key
300+ csr = os.path.join(certsdir, 'cert.csr')
301+ self.run(['openssl', 'genrsa',
302+ '-out', os.path.join(certsdir, 'privateKey.pem'),
303+ '2048'])
304+ self.run(['openssl', 'req', '-new',
305+ '-key', os.path.join(certsdir, 'privateKey.pem'),
306+ '-out', csr])
307+
308+ # generate new keys based on a csr
309+ certresp = os.path.join(self.builddir, 'certresponse.txt')
310+ self.run_to_file(self.aws + ['create-certificate-from-csr',
311+ '--certificate-signing-request', csr,
312+ '--set-as-active'],
313+ certresp)
314+ with open(certresp) as data_file:
315+ certsjson = json.load(data_file)
316+
317+ self.run_to_file(self.aws + ['describe-certificate',
318+ '--certificate-id',
319+ self.data['arn'].split(':cert/')[1],
320+ '--output',
321+ 'text',
322+ '--query',
323+ '{}Description.{}Pem'.format(
324+ 'certificate')
325+ ],
326+ os.path.join(certsdir, 'cert.pem'))
327+
328+ return certsjson
329+
330+ def build(self):
331+ certsdir = os.path.join(self.builddir, 'certs')
332+ # Make the certs directory if it does not exist
333+ os.makedirs(certsdir, exist_ok=True)
334+
335+ # What should we do with certificates?
336+ if self.options.generatekeys:
337+ self.data = self._keys_from_aws(certsdir)
338+ else:
339+ self.data = self._keys_from_local(certsdir)
340+
341+ # Extra check, but good to ensure
342+ if self.data is None:
343+ return
344+
345+ # Get the root certificate
346+ self.filename = urllib.request.urlretrieve(
347+ 'https://www.symantec.com/content/en/us/enterprise/verisign/roots/'
348+ 'VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem',
349+ filename=os.path.join(certsdir, 'rootCA.pem'))
350+
351+ # attach policy to certificate
352+ if self.options.policydocument is not '':
353+ self.pd = ('{\n'
354+ ' "Version": "2012-10-17",\n'
355+ ' "Statement": [{\n'
356+ ' "Effect": "Allow",\n'
357+ ' "Action":["iot:*"],\n'
358+ ' "Resource": ["*"]\n'
359+ ' }]\n'
360+ '}\n'
361+ )
362+ self.options.policydocument = "policydocument"
363+ with open(self.options.policydocument, "w") as text_file:
364+ text_file.write(self.pd)
365+
366+ arnresp = os.path.join(self.builddir, 'arnresponse.txt')
367+ try:
368+ self.run_to_file(self.aws + ['create-policy',
369+ '--policy-name',
370+ self.options.policyname,
371+ '--policy-document',
372+ 'file://' +
373+ self.options.policydocument],
374+ arnresp)
375+ except subprocess.CalledProcessError:
376+ print("If the policy name already exists ' \
377+ 'then creating it will fail. You can ignore this error.")
378+ self.run_to_file(self.aws + ['get-policy',
379+ '--policy-name',
380+ self.options.policyname],
381+ arnresp)
382+
383+ with open('arnresponse.txt') as data_file:
384+ self.data = json.load(data_file)
385+
386+ self.run(self.aws + ['attach-principal-policy',
387+ '-‐principal-arn',
388+ self.data["policyArn"],
389+ '--policy-name',
390+ self.options.policyname])
391+
392+ self.run(self.aws + ['create-thing',
393+ '--thing-name',
394+ self.options.thing])
395+
396+ print("Created Thing: %s" % self.options.thing)
397+
398+ def stage_fileset(self):
399+ fileset = super().stage_fileset()
400+ fileset.append('-certresponse.txt')
401+ fileset.append('-arnresponse.txt')
402+ return fileset
403
404=== modified file 'snapcraft/plugins/python2.py'
405--- snapcraft/plugins/python2.py 2015-10-27 19:23:02 +0000
406+++ snapcraft/plugins/python2.py 2015-10-29 14:24:50 +0000
407@@ -48,6 +48,15 @@
408 schema['properties']['requirements'] = {
409 'type': 'string',
410 }
411+ schema['properties']['pip-packages'] = {
412+ 'type': 'array',
413+ 'minitems': 1,
414+ 'uniqueItems': True,
415+ 'items': {
416+ 'type': 'string'
417+ },
418+ 'default': [],
419+ }
420 schema.pop('required')
421
422 return schema
423@@ -76,7 +85,8 @@
424 if self.options.requirements:
425 requirements = os.path.join(os.getcwd(), self.options.requirements)
426
427- if not os.path.exists(setup) and not self.options.requirements:
428+ if not os.path.exists(setup) and not \
429+ (self.options.requirements or self.options.pip_packages):
430 return
431
432 easy_install = os.path.join(
433@@ -100,6 +110,9 @@
434 if self.options.requirements:
435 self.run(pip_install + ['--requirement', requirements])
436
437+ if self.options.pip_packages:
438+ self.run(pip_install + ['--upgrade'] + self.options.pip_packages)
439+
440 if os.path.exists(setup):
441 self.run(pip_install + ['.', ])
442
443
444=== modified file 'snapcraft/plugins/python3.py'
445--- snapcraft/plugins/python3.py 2015-10-27 19:23:02 +0000
446+++ snapcraft/plugins/python3.py 2015-10-29 14:24:50 +0000
447@@ -48,6 +48,15 @@
448 schema['properties']['requirements'] = {
449 'type': 'string',
450 }
451+ schema['properties']['pip-packages'] = {
452+ 'type': 'array',
453+ 'minitems': 1,
454+ 'uniqueItems': True,
455+ 'items': {
456+ 'type': 'string'
457+ },
458+ 'default': [],
459+ }
460 schema.pop('required')
461
462 return schema
463@@ -76,7 +85,8 @@
464 if self.options.requirements:
465 requirements = os.path.join(os.getcwd(), self.options.requirements)
466
467- if not os.path.exists(setup) and not self.options.requirements:
468+ if not os.path.exists(setup) and not \
469+ (self.options.requirements or self.options.pip_packages):
470 return
471
472 easy_install = os.path.join(
473@@ -99,6 +109,9 @@
474 if self.options.requirements:
475 self.run(pip_install + ['--requirement', requirements])
476
477+ if self.options.pip_packages:
478+ self.run(pip_install + ['--upgrade'] + self.options.pip_packages)
479+
480 if os.path.exists(setup):
481 self.run(pip_install + ['.', ])
482
483
484=== modified file 'snapcraft/tests/test_cmds.py'
485--- snapcraft/tests/test_cmds.py 2015-10-26 15:54:30 +0000
486+++ snapcraft/tests/test_cmds.py 2015-10-29 14:24:50 +0000
487@@ -25,6 +25,7 @@
488 from snapcraft import (
489 cmds,
490 common,
491+ lifecycle,
492 tests
493 )
494
495@@ -39,16 +40,12 @@
496 self.addCleanup(tmpdirObject.cleanup)
497 tmpdir = tmpdirObject.name
498
499- part1 = mock.Mock()
500- part1.name = 'part1'
501- part1.code.options.stage = ['*']
502+ part1 = lifecycle.load_plugin('part1', 'jdk', {'source': '.'})
503 part1.installdir = tmpdir + '/install1'
504 os.makedirs(part1.installdir + '/a')
505 open(part1.installdir + '/a/1', mode='w').close()
506
507- part2 = mock.Mock()
508- part2.name = 'part2'
509- part2.code.options.stage = ['*']
510+ part2 = lifecycle.load_plugin('part2', 'jdk', {'source': '.'})
511 part2.installdir = tmpdir + '/install2'
512 os.makedirs(part2.installdir + '/a')
513 with open(part2.installdir + '/1', mode='w') as f:
514@@ -57,9 +54,7 @@
515 with open(part2.installdir + '/a/2', mode='w') as f:
516 f.write('a/2')
517
518- part3 = mock.Mock()
519- part3.name = 'part3'
520- part3.code.options.stage = ['*']
521+ part3 = lifecycle.load_plugin('part3', 'jdk', {'source': '.'})
522 part3.installdir = tmpdir + '/install3'
523 os.makedirs(part3.installdir + '/a')
524 os.makedirs(part3.installdir + '/b')
525@@ -84,6 +79,8 @@
526 def test_list_plugins(self, mock_stdout):
527 expected_list = '''ant
528 autotools
529+awscli
530+awsiot
531 catkin
532 cmake
533 copy
534
535=== modified file 'snapcraft/tests/test_lifecycle.py'
536--- snapcraft/tests/test_lifecycle.py 2015-10-21 20:03:15 +0000
537+++ snapcraft/tests/test_lifecycle.py 2015-10-29 14:24:50 +0000
538@@ -165,7 +165,7 @@
539 dstdir = tmpdir + '/stage'
540 os.makedirs(dstdir)
541
542- files, dirs = snapcraft.lifecycle.migratable_filesets(
543+ files, dirs = snapcraft.lifecycle._migratable_filesets(
544 filesets[key]['fileset'], srcdir)
545 snapcraft.lifecycle._migrate_files(files, dirs, srcdir, dstdir)
546

Subscribers

People subscribed via source and target branches

to all changes: