Merge lp:~ubuntu-branches/ubuntu/saucy/checkbox/saucy-201308281227 into lp:ubuntu/saucy/checkbox
- Saucy (13.10)
- saucy-201308281227
- Merge into saucy
Status: | Needs review |
---|---|
Proposed branch: | lp:~ubuntu-branches/ubuntu/saucy/checkbox/saucy-201308281227 |
Merge into: | lp:ubuntu/saucy/checkbox |
Diff against target: |
7511 lines (+5851/-0) (has conflicts) 141 files modified
debian/changelog (+64/-0) debian/po/ast.po (+5/-0) debian/po/cs.po (+5/-0) debian/po/de.po (+5/-0) debian/po/en_AU.po (+5/-0) debian/po/en_GB.po (+5/-0) debian/po/es.po (+5/-0) debian/po/fr.po (+5/-0) debian/po/gl.po (+5/-0) debian/po/he.po (+5/-0) debian/po/hu.po (+5/-0) debian/po/id.po (+5/-0) debian/po/it.po (+5/-0) debian/po/ja.po (+5/-0) debian/po/nl.po (+5/-0) debian/po/oc.po (+5/-0) debian/po/pl.po (+5/-0) debian/po/pt_BR.po (+5/-0) debian/po/ro.po (+5/-0) debian/po/ru.po (+5/-0) debian/po/tr.po (+5/-0) debian/po/uk.po (+5/-0) debian/po/zh_CN.po (+5/-0) debian/po/zh_TW.po (+5/-0) jobs/mediacard.txt.in (+91/-0) jobs/resource.txt.in (+6/-0) plainbox/MANIFEST.in.OTHER (+9/-0) plainbox/contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy (+30/-0) plainbox/contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy.OTHER (+29/-0) plainbox/docs/dev/architecture.rst.OTHER (+40/-0) plainbox/docs/dev/old.rst.OTHER (+343/-0) plainbox/docs/dev/reference.rst.OTHER (+179/-0) plainbox/docs/dev/resources.rst.OTHER (+259/-0) plainbox/docs/dev/trusted-launcher.rst (+209/-0) plainbox/docs/usage.rst.OTHER (+93/-0) plainbox/mk-venv.sh.OTHER (+195/-0) plainbox/plainbox/impl/box.py.OTHER (+130/-0) plainbox/plainbox/impl/commands/checkbox.py.OTHER (+100/-0) plainbox/plainbox/impl/commands/run.py.OTHER (+340/-0) plainbox/plainbox/impl/commands/special.py.OTHER (+159/-0) plainbox/plainbox/impl/commands/sru.py.OTHER (+271/-0) plainbox/plainbox/impl/commands/test_run.py.OTHER (+142/-0) plainbox/plainbox/impl/config.py.OTHER (+544/-0) plainbox/plainbox/impl/job.py.OTHER (+259/-0) plainbox/plainbox/impl/runner.py.OTHER (+421/-0) plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py.OTHER (+395/-0) plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py.OTHER (+280/-0) plainbox/plainbox/impl/test_box.py.OTHER (+260/-0) plainbox/plainbox/impl/test_job.py.OTHER (+370/-0) plainbox/setup.py.OTHER (+60/-0) po/ace.po (+5/-0) po/af.po (+5/-0) po/am.po (+5/-0) po/ar.po (+5/-0) po/ast.po (+5/-0) po/az.po (+5/-0) po/be.po (+5/-0) po/bg.po (+5/-0) po/bn.po (+5/-0) po/bo.po (+5/-0) po/br.po (+5/-0) po/bs.po (+5/-0) po/ca.po (+5/-0) po/ca@valencia.po (+5/-0) po/ckb.po (+5/-0) po/cs.po (+5/-0) po/cy.po (+5/-0) po/da.po (+5/-0) po/de.po (+5/-0) po/dv.po (+5/-0) po/el.po (+5/-0) po/en_AU.po (+5/-0) po/en_CA.po (+5/-0) po/en_GB.po (+5/-0) po/eo.po (+5/-0) po/es.po (+5/-0) po/et.po (+5/-0) po/eu.po (+5/-0) po/fa.po (+5/-0) po/fi.po (+5/-0) po/fr.po (+5/-0) po/ga.po (+5/-0) po/gd.po (+5/-0) po/gl.po (+5/-0) po/he.po (+5/-0) po/hi.po (+5/-0) po/hr.po (+5/-0) po/hu.po (+5/-0) po/hy.po (+5/-0) po/id.po (+5/-0) po/is.po (+5/-0) po/it.po (+5/-0) po/ja.po (+5/-0) po/jbo.po (+5/-0) po/ka.po (+5/-0) po/kk.po (+5/-0) po/km.po (+5/-0) po/kn.po (+5/-0) po/ko.po (+5/-0) po/ku.po (+5/-0) po/ky.po (+5/-0) po/lt.po (+5/-0) po/lv.po (+5/-0) po/mk.po (+5/-0) po/ml.po (+5/-0) po/mr.po (+5/-0) po/ms.po (+5/-0) po/my.po (+5/-0) po/nb.po (+5/-0) po/nds.po (+5/-0) po/ne.po (+5/-0) po/nl.po (+5/-0) po/nn.po (+5/-0) po/oc.po (+5/-0) po/pl.po (+5/-0) po/ps.po (+5/-0) po/pt.po (+5/-0) po/pt_BR.po (+5/-0) po/ro.po (+5/-0) po/ru.po (+5/-0) po/sd.po (+5/-0) po/shn.po (+5/-0) po/si.po (+5/-0) po/sk.po (+5/-0) po/sl.po (+5/-0) po/sq.po (+5/-0) po/sr.po (+5/-0) po/sv.po (+5/-0) po/ta.po (+5/-0) po/te.po (+5/-0) po/th.po (+5/-0) po/tr.po (+5/-0) po/ug.po (+5/-0) po/uk.po (+5/-0) po/ur.po (+5/-0) po/uz.po (+5/-0) po/vi.po (+5/-0) po/zh_CN.po (+5/-0) po/zh_HK.po (+5/-0) po/zh_TW.po (+5/-0) scripts/color_depth_info (+8/-0) Text conflict in debian/changelog Text conflict in debian/po/ast.po Text conflict in debian/po/cs.po Text conflict in debian/po/de.po Text conflict in debian/po/en_AU.po Text conflict in debian/po/en_GB.po Text conflict in debian/po/es.po Text conflict in debian/po/fr.po Text conflict in debian/po/gl.po Text conflict in debian/po/he.po Text conflict in debian/po/hu.po Text conflict in debian/po/id.po Text conflict in debian/po/it.po Text conflict in debian/po/ja.po Text conflict in debian/po/nl.po Text conflict in debian/po/oc.po Text conflict in debian/po/pl.po Text conflict in debian/po/pt_BR.po Text conflict in debian/po/ro.po Text conflict in debian/po/ru.po Text conflict in debian/po/tr.po Text conflict in debian/po/uk.po Text conflict in debian/po/zh_CN.po Text conflict in debian/po/zh_TW.po Text conflict in jobs/mediacard.txt.in Conflict adding files to plainbox. Created directory. Conflict because plainbox is not versioned, but has versioned children. Versioned directory. Contents conflict in plainbox/MANIFEST.in Contents conflict in plainbox/contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy Conflict adding files to plainbox/docs. Created directory. Conflict because plainbox/docs is not versioned, but has versioned children. Versioned directory. Conflict adding files to plainbox/docs/dev. Created directory. Conflict because plainbox/docs/dev is not versioned, but has versioned children. Versioned directory. Contents conflict in plainbox/docs/dev/architecture.rst Contents conflict in plainbox/docs/dev/old.rst Contents conflict in plainbox/docs/dev/reference.rst Contents conflict in plainbox/docs/dev/resources.rst Contents conflict in plainbox/docs/usage.rst Contents conflict in plainbox/mk-venv.sh Conflict adding files to plainbox/plainbox. Created directory. Conflict because plainbox/plainbox is not versioned, but has versioned children. Versioned directory. Conflict adding files to plainbox/plainbox/impl. Created directory. Conflict because plainbox/plainbox/impl is not versioned, but has versioned children. Versioned directory. Contents conflict in plainbox/plainbox/impl/box.py Conflict adding files to plainbox/plainbox/impl/commands. Created directory. Conflict because plainbox/plainbox/impl/commands is not versioned, but has versioned children. Versioned directory. Contents conflict in plainbox/plainbox/impl/commands/checkbox.py Contents conflict in plainbox/plainbox/impl/commands/run.py Contents conflict in plainbox/plainbox/impl/commands/special.py Contents conflict in plainbox/plainbox/impl/commands/sru.py Contents conflict in plainbox/plainbox/impl/commands/test_run.py Contents conflict in plainbox/plainbox/impl/config.py Contents conflict in plainbox/plainbox/impl/job.py Contents conflict in plainbox/plainbox/impl/runner.py Conflict adding files to plainbox/plainbox/impl/secure. Created directory. Conflict because plainbox/plainbox/impl/secure is not versioned, but has versioned children. Versioned directory. Contents conflict in plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py Contents conflict in plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py Contents conflict in plainbox/plainbox/impl/test_box.py Contents conflict in plainbox/plainbox/impl/test_job.py Contents conflict in plainbox/setup.py Text conflict in po/ace.po Text conflict in po/af.po Text conflict in po/am.po Text conflict in po/ar.po Text conflict in po/ast.po Text conflict in po/az.po Text conflict in po/be.po Text conflict in po/bg.po Text conflict in po/bn.po Text conflict in po/bo.po Text conflict in po/br.po Text conflict in po/bs.po Text conflict in po/ca.po Text conflict in po/ca@valencia.po Text conflict in po/ckb.po Text conflict in po/cs.po Text conflict in po/cy.po Text conflict in po/da.po Text conflict in po/de.po Text conflict in po/dv.po Text conflict in po/el.po Text conflict in po/en_AU.po Text conflict in po/en_CA.po Text conflict in po/en_GB.po Text conflict in po/eo.po Text conflict in po/es.po Text conflict in po/et.po Text conflict in po/eu.po Text conflict in po/fa.po Text conflict in po/fi.po Text conflict in po/fr.po Text conflict in po/ga.po Text conflict in po/gd.po Text conflict in po/gl.po Text conflict in po/he.po Text conflict in po/hi.po Text conflict in po/hr.po Text conflict in po/hu.po Text conflict in po/hy.po Text conflict in po/id.po Text conflict in po/is.po Text conflict in po/it.po Text conflict in po/ja.po Text conflict in po/jbo.po Text conflict in po/ka.po Text conflict in po/kk.po Text conflict in po/km.po Text conflict in po/kn.po Text conflict in po/ko.po Text conflict in po/ku.po Text conflict in po/ky.po Text conflict in po/lt.po Text conflict in po/lv.po Text conflict in po/mk.po Text conflict in po/ml.po Text conflict in po/mr.po Text conflict in po/ms.po Text conflict in po/my.po Text conflict in po/nb.po Text conflict in po/nds.po Text conflict in po/ne.po Text conflict in po/nl.po Text conflict in po/nn.po Text conflict in po/oc.po Text conflict in po/pl.po Text conflict in po/ps.po Text conflict in po/pt.po Text conflict in po/pt_BR.po Text conflict in po/ro.po Text conflict in po/ru.po Text conflict in po/sd.po Text conflict in po/shn.po Text conflict in po/si.po Text conflict in po/sk.po Text conflict in po/sl.po Text conflict in po/sq.po Text conflict in po/sr.po Text conflict in po/sv.po Text conflict in po/ta.po Text conflict in po/te.po Text conflict in po/th.po Text conflict in po/tr.po Text conflict in po/ug.po Text conflict in po/uk.po Text conflict in po/ur.po Text conflict in po/uz.po Text conflict in po/vi.po Text conflict in po/zh_CN.po Text conflict in po/zh_HK.po Text conflict in po/zh_TW.po Text conflict in scripts/color_depth_info |
To merge this branch: | bzr merge lp:~ubuntu-branches/ubuntu/saucy/checkbox/saucy-201308281227 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu branches | Pending | ||
Review via email: mp+182623@code.launchpad.net |
Commit message
Description of the change
The package importer has detected a possible inconsistency between the package history in the archive and the history in bzr. As the archive is authoritative the importer has made lp:ubuntu/saucy/checkbox reflect what is in the archive and the old bzr branch has been pushed to lp:~ubuntu-branches/ubuntu/saucy/checkbox/saucy-201308281227. This merge proposal was created so that an Ubuntu developer can review the situations and perform a merge/upload if necessary. There are three typical cases where this can happen.
1. Where someone pushes a change to bzr and someone else uploads the package without that change. This is the reason that this check is done by the importer. If this appears to be the case then a merge/upload should be done if the changes that were in bzr are still desirable.
2. The importer incorrectly detected the above situation when someone made a change in bzr and then uploaded it.
3. The importer incorrectly detected the above situation when someone just uploaded a package and didn't touch bzr.
If this case doesn't appear to be the first situation then set the status of the merge proposal to "Rejected" and help avoid the problem in future by filing a bug at https:/
(this is an automatically generated message)
Unmerged revisions
- 1884. By Daniel Manrique
-
* New upstream release (LP: #1180545):
* Launchpad automated translation updates
* scripts/graphics_ stress_ test, scripts/ rotation_ test: make sure to
always reset the "screen" variable. Somehow the NVIDIA driver manages
to make it unusable after the first time. (LP: #1172667)
* checkbox/parsers/ submission. py - publish kernel-release information to
interested parties.
* scripts/rendercheck_ test - change nargs='+' to action='append' for blacklist
option so it works as expected.
jobs/rendercheck. txt.in - blacklist gradients test as it is known to produce
false positives. (LP: #1093718)
* plugins/hexr_transport. py - added plugin for submitting to HEXR and
certification based on certify_new_transport from checkbox- certification.
examples/checkbox- qt.ini - blacklisted hexr_transport as we won't use it
examples/checkbox- cli.ini - blacklisted hexr_transport as we won't use it
examples/checkbox- urwid.ini - blacklisted hexr_transport as we won't use it
* Ensured that button strings from the "continue" dialog are translatable
(LP: #1176695)* checkbox/
parsers/ cpuinfo. py - split on first instance of ':' in cpuinfo
output lines to avoid splitting into more than 2 items. Also fixed a pep8
issue discovered while working on this. (LP: #1180496)
* scripts/cpu_offlining: Modified script to no longer offline cpu0 to resolve
a bug on ARM. Modified output so most of it is redirected to stderr for
fail cases, we don't need that much for success cases. (LP: #1078897)
* jobs/mediacard.txt.in: Modified test instructions to be less confusing
(LP: #970857)
* scripts/cpu_topology: define the cpuinfo nested dicts on creation rather
than define elements during parsing of /proc/cpuinfo (LP: #1111878)
* scripts/lsmod_info: Corrected error handling for the check_output() call to
trap the correct error. (LP: #1103647)
* jobs/camera.txt.in: removed an extraneous requres line for gir1.2
scripts/camera_ test: added code to determine what version of gst we're
using and set video type and plugin accordingly. (LP: #1100594)
* scripts/network_ check: added ability to specify custom target URL for
debugging failures (LP: #1128017)
* scripts/removable_ storage_ test: Added error handling to trap OSError on
non-writable media and modified output to handle subsequent
ZeroDivisionError issues when summarizing test results.
jobs/media.txt. in: Modified instructions for SD/SDHC to specify using
UNLOCKED cards to avoid issues when testing read-only media (LP: #1153894)
* jobs/suspend.txt.in, scripts/gpu_test: Remove the need of running the script
with the root user, restore the workspaces switch and the HTML5 video
playback ; remove the extra suspend/resume (LP: #1172851)
* checkbox/parsers/ udevadm. py: Only filter devices without product AND vendor
information (LP: #1167733)
Preview Diff
1 | === modified file 'checkbox/parsers/cpuinfo.py' |
2 | === modified file 'debian/changelog' |
3 | --- debian/changelog 2013-08-02 10:56:56 +0000 |
4 | +++ debian/changelog 2013-08-28 12:37:27 +0000 |
5 | @@ -1,3 +1,4 @@ |
6 | +<<<<<<< TREE |
7 | checkbox (0.16.4) saucy; urgency=low |
8 | |
9 | [ Jeff Lane ] |
10 | @@ -224,6 +225,69 @@ |
11 | |
12 | -- Daniel Manrique <roadmr@ubuntu.com> Wed, 15 May 2013 16:39:12 -0400 |
13 | |
14 | +======= |
15 | +checkbox (0.16.1) saucy; urgency=low |
16 | + |
17 | + * New upstream release (LP: #1180545): |
18 | + |
19 | + * Launchpad automated translation updates |
20 | + |
21 | + [ Alberto Milone ] |
22 | + * scripts/graphics_stress_test, scripts/rotation_test: make sure to |
23 | + always reset the "screen" variable. Somehow the NVIDIA driver manages |
24 | + to make it unusable after the first time. (LP: #1172667) |
25 | + |
26 | + [ Brendan Donegan ] |
27 | + * checkbox/parsers/submission.py - publish kernel-release information to |
28 | + interested parties. |
29 | + * scripts/rendercheck_test - change nargs='+' to action='append' for blacklist |
30 | + option so it works as expected. |
31 | + jobs/rendercheck.txt.in - blacklist gradients test as it is known to produce |
32 | + false positives. (LP: #1093718) |
33 | + * plugins/hexr_transport.py - added plugin for submitting to HEXR and |
34 | + certification based on certify_new_transport from checkbox-certification. |
35 | + examples/checkbox-qt.ini - blacklisted hexr_transport as we won't use it |
36 | + examples/checkbox-cli.ini - blacklisted hexr_transport as we won't use it |
37 | + examples/checkbox-urwid.ini - blacklisted hexr_transport as we won't use it |
38 | + |
39 | + [ Daniel Manrique ] |
40 | + * Ensured that button strings from the "continue" dialog are translatable |
41 | + (LP: #1176695) |
42 | + |
43 | + [ Jeff Lane ] |
44 | + * checkbox/parsers/cpuinfo.py - split on first instance of ':' in cpuinfo |
45 | + output lines to avoid splitting into more than 2 items. Also fixed a pep8 |
46 | + issue discovered while working on this. (LP: #1180496) |
47 | + * scripts/cpu_offlining: Modified script to no longer offline cpu0 to resolve |
48 | + a bug on ARM. Modified output so most of it is redirected to stderr for |
49 | + fail cases, we don't need that much for success cases. (LP: #1078897) |
50 | + * jobs/mediacard.txt.in: Modified test instructions to be less confusing |
51 | + (LP: #970857) |
52 | + * scripts/cpu_topology: define the cpuinfo nested dicts on creation rather |
53 | + than define elements during parsing of /proc/cpuinfo (LP: #1111878) |
54 | + * scripts/lsmod_info: Corrected error handling for the check_output() call to |
55 | + trap the correct error. (LP: #1103647) |
56 | + * jobs/camera.txt.in: removed an extraneous requres line for gir1.2 |
57 | + scripts/camera_test: added code to determine what version of gst we're |
58 | + using and set video type and plugin accordingly. (LP: #1100594) |
59 | + * scripts/network_check: added ability to specify custom target URL for |
60 | + debugging failures (LP: #1128017) |
61 | + * scripts/removable_storage_test: Added error handling to trap OSError on |
62 | + non-writable media and modified output to handle subsequent |
63 | + ZeroDivisionError issues when summarizing test results. |
64 | + jobs/media.txt.in: Modified instructions for SD/SDHC to specify using |
65 | + UNLOCKED cards to avoid issues when testing read-only media (LP: #1153894) |
66 | + |
67 | + [ Sylvain Pineau ] |
68 | + * jobs/suspend.txt.in, scripts/gpu_test: Remove the need of running the script |
69 | + with the root user, restore the workspaces switch and the HTML5 video |
70 | + playback ; remove the extra suspend/resume (LP: #1172851) |
71 | + * checkbox/parsers/udevadm.py: Only filter devices without product AND vendor |
72 | + information (LP: #1167733) |
73 | + |
74 | + -- Daniel Manrique <roadmr@ubuntu.com> Wed, 15 May 2013 16:39:12 -0400 |
75 | + |
76 | +>>>>>>> MERGE-SOURCE |
77 | checkbox (0.16) saucy; urgency=low |
78 | |
79 | * New upstream release (LP: #1178403): |
80 | |
81 | === modified file 'debian/po/ast.po' |
82 | --- debian/po/ast.po 2013-08-02 10:56:56 +0000 |
83 | +++ debian/po/ast.po 2013-08-28 12:37:27 +0000 |
84 | @@ -15,8 +15,13 @@ |
85 | "MIME-Version: 1.0\n" |
86 | "Content-Type: text/plain; charset=UTF-8\n" |
87 | "Content-Transfer-Encoding: 8bit\n" |
88 | +<<<<<<< TREE |
89 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
90 | "X-Generator: Launchpad (build 16700)\n" |
91 | +======= |
92 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
93 | +"X-Generator: Launchpad (build 16598)\n" |
94 | +>>>>>>> MERGE-SOURCE |
95 | |
96 | #. Type: string |
97 | #. Description |
98 | |
99 | === modified file 'debian/po/cs.po' |
100 | --- debian/po/cs.po 2013-08-02 10:56:56 +0000 |
101 | +++ debian/po/cs.po 2013-08-28 12:37:27 +0000 |
102 | @@ -15,8 +15,13 @@ |
103 | "MIME-Version: 1.0\n" |
104 | "Content-Type: text/plain; charset=UTF-8\n" |
105 | "Content-Transfer-Encoding: 8bit\n" |
106 | +<<<<<<< TREE |
107 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
108 | "X-Generator: Launchpad (build 16700)\n" |
109 | +======= |
110 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
111 | +"X-Generator: Launchpad (build 16598)\n" |
112 | +>>>>>>> MERGE-SOURCE |
113 | |
114 | #. Type: string |
115 | #. Description |
116 | |
117 | === modified file 'debian/po/de.po' |
118 | --- debian/po/de.po 2013-08-02 10:56:56 +0000 |
119 | +++ debian/po/de.po 2013-08-28 12:37:27 +0000 |
120 | @@ -15,8 +15,13 @@ |
121 | "MIME-Version: 1.0\n" |
122 | "Content-Type: text/plain; charset=UTF-8\n" |
123 | "Content-Transfer-Encoding: 8bit\n" |
124 | +<<<<<<< TREE |
125 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
126 | "X-Generator: Launchpad (build 16700)\n" |
127 | +======= |
128 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
129 | +"X-Generator: Launchpad (build 16598)\n" |
130 | +>>>>>>> MERGE-SOURCE |
131 | |
132 | #. Type: string |
133 | #. Description |
134 | |
135 | === modified file 'debian/po/en_AU.po' |
136 | --- debian/po/en_AU.po 2013-08-02 10:56:56 +0000 |
137 | +++ debian/po/en_AU.po 2013-08-28 12:37:27 +0000 |
138 | @@ -15,8 +15,13 @@ |
139 | "MIME-Version: 1.0\n" |
140 | "Content-Type: text/plain; charset=UTF-8\n" |
141 | "Content-Transfer-Encoding: 8bit\n" |
142 | +<<<<<<< TREE |
143 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
144 | "X-Generator: Launchpad (build 16700)\n" |
145 | +======= |
146 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
147 | +"X-Generator: Launchpad (build 16598)\n" |
148 | +>>>>>>> MERGE-SOURCE |
149 | |
150 | #. Type: string |
151 | #. Description |
152 | |
153 | === modified file 'debian/po/en_GB.po' |
154 | --- debian/po/en_GB.po 2013-08-02 10:56:56 +0000 |
155 | +++ debian/po/en_GB.po 2013-08-28 12:37:27 +0000 |
156 | @@ -15,8 +15,13 @@ |
157 | "MIME-Version: 1.0\n" |
158 | "Content-Type: text/plain; charset=UTF-8\n" |
159 | "Content-Transfer-Encoding: 8bit\n" |
160 | +<<<<<<< TREE |
161 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
162 | "X-Generator: Launchpad (build 16700)\n" |
163 | +======= |
164 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
165 | +"X-Generator: Launchpad (build 16598)\n" |
166 | +>>>>>>> MERGE-SOURCE |
167 | |
168 | #. Type: string |
169 | #. Description |
170 | |
171 | === modified file 'debian/po/es.po' |
172 | --- debian/po/es.po 2013-08-02 10:56:56 +0000 |
173 | +++ debian/po/es.po 2013-08-28 12:37:27 +0000 |
174 | @@ -15,8 +15,13 @@ |
175 | "MIME-Version: 1.0\n" |
176 | "Content-Type: text/plain; charset=UTF-8\n" |
177 | "Content-Transfer-Encoding: 8bit\n" |
178 | +<<<<<<< TREE |
179 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
180 | "X-Generator: Launchpad (build 16700)\n" |
181 | +======= |
182 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
183 | +"X-Generator: Launchpad (build 16598)\n" |
184 | +>>>>>>> MERGE-SOURCE |
185 | |
186 | #. Type: string |
187 | #. Description |
188 | |
189 | === modified file 'debian/po/fr.po' |
190 | --- debian/po/fr.po 2013-08-02 10:56:56 +0000 |
191 | +++ debian/po/fr.po 2013-08-28 12:37:27 +0000 |
192 | @@ -15,8 +15,13 @@ |
193 | "MIME-Version: 1.0\n" |
194 | "Content-Type: text/plain; charset=UTF-8\n" |
195 | "Content-Transfer-Encoding: 8bit\n" |
196 | +<<<<<<< TREE |
197 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
198 | "X-Generator: Launchpad (build 16700)\n" |
199 | +======= |
200 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
201 | +"X-Generator: Launchpad (build 16598)\n" |
202 | +>>>>>>> MERGE-SOURCE |
203 | |
204 | #. Type: string |
205 | #. Description |
206 | |
207 | === modified file 'debian/po/gl.po' |
208 | --- debian/po/gl.po 2013-08-02 10:56:56 +0000 |
209 | +++ debian/po/gl.po 2013-08-28 12:37:27 +0000 |
210 | @@ -15,8 +15,13 @@ |
211 | "MIME-Version: 1.0\n" |
212 | "Content-Type: text/plain; charset=UTF-8\n" |
213 | "Content-Transfer-Encoding: 8bit\n" |
214 | +<<<<<<< TREE |
215 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
216 | "X-Generator: Launchpad (build 16700)\n" |
217 | +======= |
218 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
219 | +"X-Generator: Launchpad (build 16598)\n" |
220 | +>>>>>>> MERGE-SOURCE |
221 | |
222 | #. Type: string |
223 | #. Description |
224 | |
225 | === modified file 'debian/po/he.po' |
226 | --- debian/po/he.po 2013-08-02 10:56:56 +0000 |
227 | +++ debian/po/he.po 2013-08-28 12:37:27 +0000 |
228 | @@ -15,8 +15,13 @@ |
229 | "MIME-Version: 1.0\n" |
230 | "Content-Type: text/plain; charset=UTF-8\n" |
231 | "Content-Transfer-Encoding: 8bit\n" |
232 | +<<<<<<< TREE |
233 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
234 | "X-Generator: Launchpad (build 16700)\n" |
235 | +======= |
236 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
237 | +"X-Generator: Launchpad (build 16598)\n" |
238 | +>>>>>>> MERGE-SOURCE |
239 | |
240 | #. Type: string |
241 | #. Description |
242 | |
243 | === modified file 'debian/po/hu.po' |
244 | --- debian/po/hu.po 2013-08-02 10:56:56 +0000 |
245 | +++ debian/po/hu.po 2013-08-28 12:37:27 +0000 |
246 | @@ -15,8 +15,13 @@ |
247 | "MIME-Version: 1.0\n" |
248 | "Content-Type: text/plain; charset=UTF-8\n" |
249 | "Content-Transfer-Encoding: 8bit\n" |
250 | +<<<<<<< TREE |
251 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
252 | "X-Generator: Launchpad (build 16700)\n" |
253 | +======= |
254 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
255 | +"X-Generator: Launchpad (build 16598)\n" |
256 | +>>>>>>> MERGE-SOURCE |
257 | |
258 | #. Type: string |
259 | #. Description |
260 | |
261 | === modified file 'debian/po/id.po' |
262 | --- debian/po/id.po 2013-08-02 10:56:56 +0000 |
263 | +++ debian/po/id.po 2013-08-28 12:37:27 +0000 |
264 | @@ -15,8 +15,13 @@ |
265 | "MIME-Version: 1.0\n" |
266 | "Content-Type: text/plain; charset=UTF-8\n" |
267 | "Content-Transfer-Encoding: 8bit\n" |
268 | +<<<<<<< TREE |
269 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
270 | "X-Generator: Launchpad (build 16700)\n" |
271 | +======= |
272 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
273 | +"X-Generator: Launchpad (build 16598)\n" |
274 | +>>>>>>> MERGE-SOURCE |
275 | |
276 | #. Type: string |
277 | #. Description |
278 | |
279 | === modified file 'debian/po/it.po' |
280 | --- debian/po/it.po 2013-08-02 10:56:56 +0000 |
281 | +++ debian/po/it.po 2013-08-28 12:37:27 +0000 |
282 | @@ -15,8 +15,13 @@ |
283 | "MIME-Version: 1.0\n" |
284 | "Content-Type: text/plain; charset=UTF-8\n" |
285 | "Content-Transfer-Encoding: 8bit\n" |
286 | +<<<<<<< TREE |
287 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
288 | "X-Generator: Launchpad (build 16700)\n" |
289 | +======= |
290 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
291 | +"X-Generator: Launchpad (build 16598)\n" |
292 | +>>>>>>> MERGE-SOURCE |
293 | |
294 | #. Type: string |
295 | #. Description |
296 | |
297 | === modified file 'debian/po/ja.po' |
298 | --- debian/po/ja.po 2013-08-02 10:56:56 +0000 |
299 | +++ debian/po/ja.po 2013-08-28 12:37:27 +0000 |
300 | @@ -15,8 +15,13 @@ |
301 | "MIME-Version: 1.0\n" |
302 | "Content-Type: text/plain; charset=UTF-8\n" |
303 | "Content-Transfer-Encoding: 8bit\n" |
304 | +<<<<<<< TREE |
305 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
306 | "X-Generator: Launchpad (build 16700)\n" |
307 | +======= |
308 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
309 | +"X-Generator: Launchpad (build 16598)\n" |
310 | +>>>>>>> MERGE-SOURCE |
311 | |
312 | #. Type: string |
313 | #. Description |
314 | |
315 | === modified file 'debian/po/nl.po' |
316 | --- debian/po/nl.po 2013-08-02 10:56:56 +0000 |
317 | +++ debian/po/nl.po 2013-08-28 12:37:27 +0000 |
318 | @@ -15,8 +15,13 @@ |
319 | "MIME-Version: 1.0\n" |
320 | "Content-Type: text/plain; charset=UTF-8\n" |
321 | "Content-Transfer-Encoding: 8bit\n" |
322 | +<<<<<<< TREE |
323 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
324 | "X-Generator: Launchpad (build 16700)\n" |
325 | +======= |
326 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
327 | +"X-Generator: Launchpad (build 16598)\n" |
328 | +>>>>>>> MERGE-SOURCE |
329 | |
330 | #. Type: string |
331 | #. Description |
332 | |
333 | === modified file 'debian/po/oc.po' |
334 | --- debian/po/oc.po 2013-08-02 10:56:56 +0000 |
335 | +++ debian/po/oc.po 2013-08-28 12:37:27 +0000 |
336 | @@ -15,8 +15,13 @@ |
337 | "MIME-Version: 1.0\n" |
338 | "Content-Type: text/plain; charset=UTF-8\n" |
339 | "Content-Transfer-Encoding: 8bit\n" |
340 | +<<<<<<< TREE |
341 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
342 | "X-Generator: Launchpad (build 16700)\n" |
343 | +======= |
344 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
345 | +"X-Generator: Launchpad (build 16598)\n" |
346 | +>>>>>>> MERGE-SOURCE |
347 | |
348 | #. Type: string |
349 | #. Description |
350 | |
351 | === modified file 'debian/po/pl.po' |
352 | --- debian/po/pl.po 2013-08-02 10:56:56 +0000 |
353 | +++ debian/po/pl.po 2013-08-28 12:37:27 +0000 |
354 | @@ -15,8 +15,13 @@ |
355 | "MIME-Version: 1.0\n" |
356 | "Content-Type: text/plain; charset=UTF-8\n" |
357 | "Content-Transfer-Encoding: 8bit\n" |
358 | +<<<<<<< TREE |
359 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
360 | "X-Generator: Launchpad (build 16700)\n" |
361 | +======= |
362 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
363 | +"X-Generator: Launchpad (build 16598)\n" |
364 | +>>>>>>> MERGE-SOURCE |
365 | |
366 | #. Type: string |
367 | #. Description |
368 | |
369 | === modified file 'debian/po/pt_BR.po' |
370 | --- debian/po/pt_BR.po 2013-08-02 10:56:56 +0000 |
371 | +++ debian/po/pt_BR.po 2013-08-28 12:37:27 +0000 |
372 | @@ -15,8 +15,13 @@ |
373 | "MIME-Version: 1.0\n" |
374 | "Content-Type: text/plain; charset=UTF-8\n" |
375 | "Content-Transfer-Encoding: 8bit\n" |
376 | +<<<<<<< TREE |
377 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
378 | "X-Generator: Launchpad (build 16700)\n" |
379 | +======= |
380 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
381 | +"X-Generator: Launchpad (build 16598)\n" |
382 | +>>>>>>> MERGE-SOURCE |
383 | |
384 | #. Type: string |
385 | #. Description |
386 | |
387 | === modified file 'debian/po/ro.po' |
388 | --- debian/po/ro.po 2013-08-02 10:56:56 +0000 |
389 | +++ debian/po/ro.po 2013-08-28 12:37:27 +0000 |
390 | @@ -15,8 +15,13 @@ |
391 | "MIME-Version: 1.0\n" |
392 | "Content-Type: text/plain; charset=UTF-8\n" |
393 | "Content-Transfer-Encoding: 8bit\n" |
394 | +<<<<<<< TREE |
395 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
396 | "X-Generator: Launchpad (build 16700)\n" |
397 | +======= |
398 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
399 | +"X-Generator: Launchpad (build 16598)\n" |
400 | +>>>>>>> MERGE-SOURCE |
401 | |
402 | #. Type: string |
403 | #. Description |
404 | |
405 | === modified file 'debian/po/ru.po' |
406 | --- debian/po/ru.po 2013-08-02 10:56:56 +0000 |
407 | +++ debian/po/ru.po 2013-08-28 12:37:27 +0000 |
408 | @@ -15,8 +15,13 @@ |
409 | "MIME-Version: 1.0\n" |
410 | "Content-Type: text/plain; charset=UTF-8\n" |
411 | "Content-Transfer-Encoding: 8bit\n" |
412 | +<<<<<<< TREE |
413 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
414 | "X-Generator: Launchpad (build 16700)\n" |
415 | +======= |
416 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
417 | +"X-Generator: Launchpad (build 16598)\n" |
418 | +>>>>>>> MERGE-SOURCE |
419 | |
420 | #. Type: string |
421 | #. Description |
422 | |
423 | === modified file 'debian/po/tr.po' |
424 | --- debian/po/tr.po 2013-08-02 10:56:56 +0000 |
425 | +++ debian/po/tr.po 2013-08-28 12:37:27 +0000 |
426 | @@ -15,8 +15,13 @@ |
427 | "MIME-Version: 1.0\n" |
428 | "Content-Type: text/plain; charset=UTF-8\n" |
429 | "Content-Transfer-Encoding: 8bit\n" |
430 | +<<<<<<< TREE |
431 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
432 | "X-Generator: Launchpad (build 16700)\n" |
433 | +======= |
434 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
435 | +"X-Generator: Launchpad (build 16598)\n" |
436 | +>>>>>>> MERGE-SOURCE |
437 | |
438 | #. Type: string |
439 | #. Description |
440 | |
441 | === modified file 'debian/po/uk.po' |
442 | --- debian/po/uk.po 2013-08-02 10:56:56 +0000 |
443 | +++ debian/po/uk.po 2013-08-28 12:37:27 +0000 |
444 | @@ -15,8 +15,13 @@ |
445 | "MIME-Version: 1.0\n" |
446 | "Content-Type: text/plain; charset=UTF-8\n" |
447 | "Content-Transfer-Encoding: 8bit\n" |
448 | +<<<<<<< TREE |
449 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
450 | "X-Generator: Launchpad (build 16700)\n" |
451 | +======= |
452 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
453 | +"X-Generator: Launchpad (build 16598)\n" |
454 | +>>>>>>> MERGE-SOURCE |
455 | |
456 | #. Type: string |
457 | #. Description |
458 | |
459 | === modified file 'debian/po/zh_CN.po' |
460 | --- debian/po/zh_CN.po 2013-08-02 10:56:56 +0000 |
461 | +++ debian/po/zh_CN.po 2013-08-28 12:37:27 +0000 |
462 | @@ -15,8 +15,13 @@ |
463 | "MIME-Version: 1.0\n" |
464 | "Content-Type: text/plain; charset=UTF-8\n" |
465 | "Content-Transfer-Encoding: 8bit\n" |
466 | +<<<<<<< TREE |
467 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
468 | "X-Generator: Launchpad (build 16700)\n" |
469 | +======= |
470 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
471 | +"X-Generator: Launchpad (build 16598)\n" |
472 | +>>>>>>> MERGE-SOURCE |
473 | |
474 | #. Type: string |
475 | #. Description |
476 | |
477 | === modified file 'debian/po/zh_TW.po' |
478 | --- debian/po/zh_TW.po 2013-08-02 10:56:56 +0000 |
479 | +++ debian/po/zh_TW.po 2013-08-28 12:37:27 +0000 |
480 | @@ -15,8 +15,13 @@ |
481 | "MIME-Version: 1.0\n" |
482 | "Content-Type: text/plain; charset=UTF-8\n" |
483 | "Content-Transfer-Encoding: 8bit\n" |
484 | +<<<<<<< TREE |
485 | "X-Launchpad-Export-Date: 2013-07-24 05:13+0000\n" |
486 | "X-Generator: Launchpad (build 16700)\n" |
487 | +======= |
488 | +"X-Launchpad-Export-Date: 2013-05-09 05:18+0000\n" |
489 | +"X-Generator: Launchpad (build 16598)\n" |
490 | +>>>>>>> MERGE-SOURCE |
491 | |
492 | #. Type: string |
493 | #. Description |
494 | |
495 | === modified file 'jobs/graphics.txt.in' |
496 | === modified file 'jobs/mediacard.txt.in' |
497 | --- jobs/mediacard.txt.in 2013-08-02 10:56:56 +0000 |
498 | +++ jobs/mediacard.txt.in 2013-08-28 12:37:27 +0000 |
499 | @@ -78,6 +78,52 @@ |
500 | The verification of this test is automated. Do not change the |
501 | automatically selected result. |
502 | |
503 | +<<<<<<< TREE |
504 | +======= |
505 | +plugin: user-interact |
506 | +name: mediacard/sd-insert-after-suspend |
507 | +depends: suspend/suspend_advanced |
508 | +command: removable_storage_watcher --memorycard insert sdio usb scsi |
509 | +_description: |
510 | + PURPOSE: |
511 | + This test will check that the systems media card reader can |
512 | + detect the insertion of an UNLOCKED SD card after the system |
513 | + has been suspended |
514 | + STEPS: |
515 | + 1. Click "Test" and insert an UNLOCKED SD card into the reader. |
516 | + If a file browser opens up, you can safely close it. |
517 | + (Note: this test will time-out after 20 seconds.) |
518 | + 2. Do not remove the device after this test. |
519 | + VERIFICATION: |
520 | + The verification of this test is automated. Do not change the |
521 | + automatically selected result. |
522 | + |
523 | +plugin: shell |
524 | +name: mediacard/sd-storage-after-suspend |
525 | +depends: mediacard/sd-insert-after-suspend |
526 | +user: root |
527 | +command: removable_storage_test -s 268400000 --memorycard sdio usb scsi |
528 | +_description: |
529 | + This test is automated and executes after the mediacard/sd-insert-after-suspend test |
530 | + is run. It tests reading and writing to the SD card after the system has been suspended. |
531 | + |
532 | +plugin: user-interact |
533 | +name: mediacard/sd-remove-after-suspend |
534 | +depends: mediacard/sd-insert-after-suspend |
535 | +command: removable_storage_watcher --memorycard remove sdio usb scsi |
536 | +_description: |
537 | + PURPOSE: |
538 | + This test will check that the system correctly detects |
539 | + the removal of an SD card from the systems card reader |
540 | + after the system has been suspended. |
541 | + STEPS: |
542 | + 1. Click "Test" and remove the SD card from the reader. |
543 | + (Note: this test will time-out after 20 seconds.) |
544 | + VERIFICATION: |
545 | + The verification of this test is automated. Do not change the |
546 | + automatically selected result. |
547 | + |
548 | +>>>>>>> MERGE-SOURCE |
549 | plugin: shell |
550 | name: mediacard/sd-preinserted |
551 | user: root |
552 | @@ -130,6 +176,51 @@ |
553 | automatically selected result. |
554 | |
555 | plugin: user-interact |
556 | +<<<<<<< TREE |
557 | +======= |
558 | +name: mediacard/sdhc-insert-after-suspend |
559 | +depends: suspend/suspend_advanced |
560 | +command: removable_storage_watcher --memorycard insert sdio usb scsi |
561 | +_description: |
562 | + PURPOSE: |
563 | + This test will check that the systems media card reader can |
564 | + detect the insertion of an UNLOCKED SDHC media card after the |
565 | + system has been suspended |
566 | + STEPS: |
567 | + 1. Click "Test" and insert an UNLOCKED SDHC card into the reader. |
568 | + If a file browser opens up, you can safely close it. |
569 | + (Note: this test will time-out after 20 seconds.) |
570 | + 2. Do not remove the device after this test. |
571 | + VERIFICATION: |
572 | + The verification of this test is automated. Do not change the |
573 | + automatically selected result. |
574 | + |
575 | +plugin: shell |
576 | +name: mediacard/sdhc-storage-after-suspend |
577 | +depends: mediacard/sdhc-insert-after-suspend |
578 | +user: root |
579 | +command: removable_storage_test -s 268400000 --memorycard sdio usb scsi |
580 | +_description: |
581 | + This test is automated and executes after the mediacard/sdhc-insert-after-suspend test |
582 | + is run. It tests reading and writing to the SDHC card after the system has been suspended. |
583 | + |
584 | +plugin: user-interact |
585 | +name: mediacard/sdhc-remove-after-suspend |
586 | +depends: mediacard/sdhc-insert-after-suspend |
587 | +command: removable_storage_watcher --memorycard remove sdio usb scsi |
588 | +_description: |
589 | + PURPOSE: |
590 | + This test will check that the system correctly detects the removal |
591 | + of an SDHC card from the systems card reader after the system has been suspended. |
592 | + STEPS: |
593 | + 1. Click "Test" and remove the SDHC card from the reader. |
594 | + (Note: this test will time-out after 20 seconds.) |
595 | + VERIFICATION: |
596 | + The verification of this test is automated. Do not change the |
597 | + automatically selected result. |
598 | + |
599 | +plugin: user-interact |
600 | +>>>>>>> MERGE-SOURCE |
601 | name: mediacard/cf-insert |
602 | command: removable_storage_watcher --memorycard insert sdio usb scsi |
603 | _description: |
604 | |
605 | === modified file 'jobs/resource.txt.in' |
606 | --- jobs/resource.txt.in 2013-07-12 14:51:00 +0000 |
607 | +++ jobs/resource.txt.in 2013-08-28 12:37:27 +0000 |
608 | @@ -56,6 +56,12 @@ |
609 | command: |
610 | find -H $(echo "$PATH" | sed -e 's/:/ /g') -maxdepth 1 -type f -executable -printf "name: %f\n\n" |
611 | |
612 | +name: executable |
613 | +plugin: resource |
614 | +description: Generates a resource for all available executables |
615 | +command: |
616 | + find -H $(echo "$PATH" | sed -e 's/:/ /g') -maxdepth 1 -type f -executable -printf "name: %f\n\n" |
617 | + |
618 | name: device |
619 | plugin: resource |
620 | command: udev_resource |
621 | |
622 | === added directory 'plainbox' |
623 | === added file 'plainbox/MANIFEST.in.OTHER' |
624 | --- plainbox/MANIFEST.in.OTHER 1970-01-01 00:00:00 +0000 |
625 | +++ plainbox/MANIFEST.in.OTHER 2013-08-28 12:37:27 +0000 |
626 | @@ -0,0 +1,9 @@ |
627 | +include README.md |
628 | +include COPYING |
629 | +include mk-interesting-graphs.sh |
630 | +recursive-include plainbox/test-data/ *.json *.xml *.txt |
631 | +recursive-include docs *.rst |
632 | +include docs/conf.py |
633 | +include plainbox/data/report/hardware-1_0.rng |
634 | +include contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy |
635 | +include contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy |
636 | |
637 | === added directory 'plainbox/contrib' |
638 | === added directory 'plainbox/contrib/policykit_auth_admin_keep' |
639 | === added file 'plainbox/contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy' |
640 | --- plainbox/contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy 1970-01-01 00:00:00 +0000 |
641 | +++ plainbox/contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy 2013-08-28 12:37:27 +0000 |
642 | @@ -0,0 +1,30 @@ |
643 | +<?xml version="1.0" encoding="UTF-8"?> |
644 | +<!DOCTYPE policyconfig PUBLIC |
645 | + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" |
646 | + "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> |
647 | +<policyconfig> |
648 | + |
649 | + <!-- |
650 | + Policy definitions for PlainBox system actions. |
651 | + (C) 2013 Canonical Ltd. |
652 | + Author: Sylvain Pineau <sylvain.pineau@canonical.com> |
653 | + --> |
654 | + |
655 | + <vendor>PlainBox</vendor> |
656 | + <vendor_url>https://launchpad.net/checkbox</vendor_url> |
657 | + <icon_name>checkbox</icon_name> |
658 | + |
659 | + <action id="org.freedesktop.policykit.pkexec.run-plainbox-job"> |
660 | + <description>Run Job command</description> |
661 | + <message>Please enter your password. Some tests require root access to run properly. Your password will never be stored and will never be submitted with test results.</message> |
662 | + <defaults> |
663 | + <allow_any>no</allow_any> |
664 | + <allow_inactive>no</allow_inactive> |
665 | + <allow_active>auth_admin_keep</allow_active> |
666 | + </defaults> |
667 | + <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/checkbox-trusted-launcher</annotate> |
668 | + <annotate key="org.freedesktop.policykit.exec.allow_gui">TRUE</annotate> |
669 | + </action> |
670 | + |
671 | +</policyconfig> |
672 | + |
673 | |
674 | === added directory 'plainbox/contrib/policykit_yes' |
675 | === added file 'plainbox/contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy.OTHER' |
676 | --- plainbox/contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy.OTHER 1970-01-01 00:00:00 +0000 |
677 | +++ plainbox/contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy.OTHER 2013-08-28 12:37:27 +0000 |
678 | @@ -0,0 +1,29 @@ |
679 | +<?xml version="1.0" encoding="UTF-8"?> |
680 | +<!DOCTYPE policyconfig PUBLIC |
681 | + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" |
682 | + "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> |
683 | +<policyconfig> |
684 | + |
685 | + <!-- |
686 | + Policy definitions for PlainBox system actions. |
687 | + (C) 2013 Canonical Ltd. |
688 | + Author: Sylvain Pineau <sylvain.pineau@canonical.com> |
689 | + --> |
690 | + |
691 | + <vendor>PlainBox</vendor> |
692 | + <vendor_url>https://launchpad.net/checkbox</vendor_url> |
693 | + <icon_name>checkbox</icon_name> |
694 | + |
695 | + <action id="org.freedesktop.policykit.pkexec.run-plainbox-job"> |
696 | + <description>Run Job command</description> |
697 | + <defaults> |
698 | + <allow_any>no</allow_any> |
699 | + <allow_inactive>no</allow_inactive> |
700 | + <allow_active>yes</allow_active> |
701 | + </defaults> |
702 | + <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/checkbox-trusted-launcher</annotate> |
703 | + <annotate key="org.freedesktop.policykit.exec.allow_gui">TRUE</annotate> |
704 | + </action> |
705 | + |
706 | +</policyconfig> |
707 | + |
708 | |
709 | === added directory 'plainbox/docs' |
710 | === added directory 'plainbox/docs/dev' |
711 | === added file 'plainbox/docs/dev/architecture.rst.OTHER' |
712 | --- plainbox/docs/dev/architecture.rst.OTHER 1970-01-01 00:00:00 +0000 |
713 | +++ plainbox/docs/dev/architecture.rst.OTHER 2013-08-28 12:37:27 +0000 |
714 | @@ -0,0 +1,40 @@ |
715 | +PlainBox Architecture |
716 | +===================== |
717 | + |
718 | +This document explains the architecture of PlainBox internals. It should be |
719 | +always up-to-date and accurate to the extent of the scope of this overview. |
720 | + |
721 | +.. toctree:: |
722 | + :maxdepth: 3 |
723 | + |
724 | + trusted-launcher.rst |
725 | + config.rst |
726 | + resources.rst |
727 | + old.rst |
728 | + |
729 | +General design considerations |
730 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
731 | + |
732 | +PlainBox is a reimplementation of CheckBox that replaces a reactor / event / |
733 | +plugin architecture with a monolithic core and tightly integrated components. |
734 | + |
735 | +The implementation models a few of the externally-visible concepts such as |
736 | +jobs, resources and resource programs but also has some additional design that |
737 | +was not present in CheckBox before. |
738 | + |
739 | +The goal of the rewrite is to provide the right model and APIs for user |
740 | +interfaces in order to build the kind of end-user solution that we could not |
741 | +build with CheckBox. |
742 | + |
743 | +This is expressed by additional functionality that is there only to provide the |
744 | +higher layers with the right data (failure reason, descriptions, etc.). The |
745 | +code is also intended to be highly testable. Test coverage at the time of |
746 | +writing this document was exceeding 80% |
747 | + |
748 | +The core requirement for the current phase of PlainBox development is feature |
749 | +parity with CheckBox and gradual shift from one to another in the daily |
750 | +responsibilities of the Hardware Certification team. Currently PlainBox |
751 | +implements a large chunk of core / essential features from CheckBox. While not |
752 | +all features are present the core is considered almost feature complete at this |
753 | +stage. |
754 | + |
755 | |
756 | === added file 'plainbox/docs/dev/old.rst.OTHER' |
757 | --- plainbox/docs/dev/old.rst.OTHER 1970-01-01 00:00:00 +0000 |
758 | +++ plainbox/docs/dev/old.rst.OTHER 2013-08-28 12:37:27 +0000 |
759 | @@ -0,0 +1,343 @@ |
760 | +Old Architecture Notes |
761 | +====================== |
762 | + |
763 | +.. warning:: |
764 | + |
765 | + This section needs maintenance |
766 | + |
767 | +Application Skeleton |
768 | +^^^^^^^^^^^^^^^^^^^^ |
769 | + |
770 | +This skeleton represents a typical application based on PlainBox. It enumerates |
771 | +the essential parts of the APIs from the point of view of an application |
772 | +developer. |
773 | + |
774 | +1. Instantiate :class:`plainbox.impl.checkbox.CheckBox` then call |
775 | + :meth:`plainbox.impl.checkbox.CheckBox.get_builtin_jobs()` to discover all |
776 | + known jobs. In the future this might be replaced by a step that obtains jobs |
777 | + from a named provider. |
778 | + |
779 | +3. Instantiate :class:`plainbox.impl.runner.JobRunner` so that we can run jobs |
780 | + |
781 | +4. Instantiate :class:`plainbox.impl.session.SessionState` so that we can keep |
782 | + track of application state. |
783 | + |
784 | + - Potentially restore an earlier, interrupted, testing session by calling |
785 | + :meth:`plainbox.impl.session.SessionState.restore()` |
786 | + |
787 | + - Potentially remove an earlier, interrupted, testing session by calling |
788 | + :meth:`plainbox.impl.session.SessionState.discard()` |
789 | + |
790 | + - Potentially start a new test session by calling |
791 | + :meth:`plainbox.impl.session.SessionState.open()` |
792 | + |
793 | +5. Allow the user to select jobs that should be executed and update session |
794 | + state by calling |
795 | + :meth:`plainbox.impl.session.SessionState.update_desired_job_list()` |
796 | + |
797 | +6. For each job in :attr:`plainbox.impl.SessionState.run_list`: |
798 | + |
799 | + 1. Check if we want to run the job (if we have a result for it from previous |
800 | + runs) or if we must run it (for jobs that cannot be persisted across |
801 | + suspend) |
802 | + |
803 | + 2. Check if the job can be started by looking at |
804 | + :meth:`plainbox.impl.session.JobState.can_start()` |
805 | + |
806 | + - optionally query for additional data on why a job cannot be started and |
807 | + present that to the user. |
808 | + |
809 | + - optionally abort the sequence and go to step 5 or the outer loop. |
810 | + |
811 | + 3. Call :meth:`plainbox.impl.runner.JobRunner.run_job()` with the current |
812 | + job and store the result. |
813 | + |
814 | + - optionally ask the user to perform some manipulation |
815 | + |
816 | + - optionally ask the user to qualify the outcome |
817 | + |
818 | + - optionally ask the user for additional comments |
819 | + |
820 | + 4. Call :meth:`plainbox.impl.session.SessionState.update_job_result()` to |
821 | + update readiness of jobs that depend on the outcome or output of current |
822 | + job. |
823 | + |
824 | + 5. Call :meth:`plainbox.impl.session.SessionState.checkpoint()` to ensure |
825 | + that testing can resume after system crash or shutdown. |
826 | + |
827 | +7. Instantiate the selected state exporter, for example |
828 | + :class:`plainbox.impl.exporters.json.JSONSessionStateExporter` so that we |
829 | + can use it to save test results. |
830 | + |
831 | + - optionally pass configuration options to customize the subset and the |
832 | + presentation of the session state |
833 | + |
834 | +8. Call |
835 | + :meth:`plainbox.impl.exporters.SessionStateExporterBase.get_session_data_subset()` |
836 | + followed by :meth:`plainbox.impl.exporters.SessionStateExporterBase.dump()` |
837 | + to save results to a file. |
838 | + |
839 | +9. Call :meth:`plainbox.impl.session.SessionState.close()` to remove any |
840 | + nonvolatile temporary storage that was needed for the session. |
841 | + |
842 | +Essential classes |
843 | +================= |
844 | + |
845 | +:class:`~plainbox.impl.session.SessionState` |
846 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
847 | + |
848 | +Class representing all state needed during a single program session. |
849 | + |
850 | +Usage |
851 | +----- |
852 | + |
853 | +The general idea is that you feed the session with a list of known jobs and |
854 | +a subset of jobs that you want to run and in return get an ordered list of |
855 | +jobs to run. |
856 | + |
857 | +It is expected that the user will select / deselect and run jobs. This |
858 | +class can react to both actions by recomputing the dependency graph and |
859 | +updating the read states accordingly. |
860 | + |
861 | +As the user runs subsequent jobs the results of those jobs are exposed to |
862 | +the session with :meth:`update_job_result()`. This can cause subsequent |
863 | +jobs to become available (not inhibited by anything). Note that there is no |
864 | +notification of changes at this time. |
865 | + |
866 | +The session does almost nothing by itself, it learns about everything by |
867 | +observing job results coming from the job runner |
868 | +(:class:`plainbox.impl.runner.JobRunner`) that applications need to |
869 | +instantiate. |
870 | + |
871 | +Suspend and resume |
872 | +------------------ |
873 | + |
874 | +The session can save check-point data after each job is executed. This |
875 | +allows the system to survive and continue after a catastrophic failure |
876 | +(broken suspend, power failure) or continue across tests that require the |
877 | +machine to reboot. |
878 | + |
879 | +.. todo:: |
880 | + |
881 | + Create a section on suspend/resume design |
882 | + |
883 | +Implementation notes |
884 | +-------------------- |
885 | + |
886 | +Internally it ties into :class:`plainbox.impl.depmgr.DependencySolver` for |
887 | +resolving dependencies. The way the session objects are used allows them to |
888 | +return various problems back to the UI level - those are all the error |
889 | +classes from :mod:`plainbox.impl.depmgr`: |
890 | + |
891 | + - :class:`plainbox.impl.depmgr.DependencyCycleError` |
892 | + |
893 | + - :class:`plainbox.impl.depmgr.DependencyDuplicateError` |
894 | + |
895 | + - :class:`plainbox.impl.depmgr.DependencyMissingError` |
896 | + |
897 | +Normally *none* of those errors should ever happen, they are only provided |
898 | +so that we don't choke when a problem really happens. Everything is checked |
899 | +and verified early before starting a job so typical unit and integration |
900 | +testing should capture broken job definitions (for example, with cyclic |
901 | +dependencies) being added to the repository. |
902 | + |
903 | +Implementation issues |
904 | +--------------------- |
905 | + |
906 | +There are two issues that are known at this time: |
907 | + |
908 | +* There is too much checkbox-specific knowledge which really belongs |
909 | + elsewhere. We are working to remove that so that non-checkbox jobs |
910 | + can be introduced later. There is a branch in progress that entirely |
911 | + removes that and moves it to a new concept called SessionController. |
912 | + In that design the session delegates understanding of results to a |
913 | + per-job session controller and exposes some APIs to alter the state |
914 | + that was previously internal (most notably a way to add new jobs and |
915 | + resources). |
916 | + |
917 | +* The way jobs are currently selected is unfortunate because of local jobs |
918 | + that can add new jobs to the system. This causes considerable complexity |
919 | + at the application level where the application must check if each |
920 | + executed job is a 'local' job and re-compute the desired_job_list. This |
921 | + should be replaced by a matcher function that can be passed to |
922 | + SessionState once so that desired_job_list is re-evaluated internally |
923 | + whenever job_list changes. |
924 | + |
925 | + |
926 | +:class:`~plainbox.impl.job.JobDefinition` |
927 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
928 | + |
929 | +:term:`CheckBox` has a concept of a :term:`job`. Jobs are named units of |
930 | +testing work that can be executed. Typical jobs range from automated CPU power |
931 | +management checks, BIOS tests, semi-automated peripherals testing to all manual |
932 | +validation by following a script (intended for humans). |
933 | + |
934 | +Jobs are distributed in plain text files, formated as a loose RFC822 documents |
935 | +where typically a single text file contains a few dozen different jobs that |
936 | +belong to one topic, for example, all bluetooth tests. |
937 | + |
938 | +Tests have a number of properties that will not be discussed in detail here, |
939 | +they are all documented in :class:`plainbox.impl.job.JobDefinition`. From the |
940 | +architecture point of view the four essential properties of a job are *name*, |
941 | +*plugin* and *requires* and *depends*. Those are discussed in detail below. |
942 | + |
943 | +JobDefinition.name |
944 | +------------------ |
945 | + |
946 | +The *name* field must be unique and is referred to by other parts of the system |
947 | +(such as whitelists). Typically jobs follow a simple naming pattern |
948 | +'category/detail', eg, 'networking/modem_connection'. The name must be _unique_ |
949 | +and this is enforced by the core. |
950 | + |
951 | +JobDefinition.plugin |
952 | +-------------------- |
953 | + |
954 | +The *plugin* field is an archaism from CheckBox and a misnomer (as PlainBox |
955 | +does not have any plugins). In the CheckBox architecture it would instruct the |
956 | +core which plugin should process that job. In PlainBox it is a way to encode |
957 | +what type of a job is being processed. There is a finite set of types that are |
958 | +documented below. |
959 | + |
960 | +plugin == "shell" |
961 | +################# |
962 | + |
963 | +This value is used for fully automated jobs. Everything the job needs to do is |
964 | +automated (preparation, execution, verification) and fully handled by the |
965 | +command that is associated with a job. |
966 | + |
967 | +plugin == "manual" |
968 | +################## |
969 | + |
970 | +This value is used for fully manual jobs. It has no special handling in the core |
971 | +apart from requiring a human-provided outcome (pass/fail classification) |
972 | + |
973 | +.. _local: |
974 | + |
975 | +plugin == "local" |
976 | +################# |
977 | + |
978 | +This value is used for special job generator jobs. The output of such jobs is |
979 | +interpreted as additional jobs and is identical in effect to loading such jobs |
980 | +from a job definition file. |
981 | + |
982 | +There are two practical uses for such jobs: |
983 | + |
984 | +* Some local jobs are used to generate a number of jobs for each object. |
985 | + This is needed where the tested machine may have a number of such objects |
986 | + and each requires unique testing. A good example is a computer where all |
987 | + network tests are explicitly "instantiated" for each network card |
988 | + present. |
989 | + |
990 | + This is a valid use case but is rather unfortunate for architecture of |
991 | + PlainBox and there is a desire to replace it with equally-expressive |
992 | + pattern jobs. The advantage is that unlike local jobs (which cannot be |
993 | + "discovered" without enduring any potential side effects that may be |
994 | + caused by the job script command) pattern jobs would allow the core to |
995 | + determine the names of jobs that can be generated and, for example, |
996 | + automatically determine that a pattern job needs to be executed as a |
997 | + dependency of a phantom (yet undetermined) job with a given name. |
998 | + |
999 | + The solution with "pattern" jobs may be executed in future phases of |
1000 | + PlainBox development. Currently there is no support for that at all. |
1001 | + |
1002 | + Currently PlainBox cannot determine job dependencies across local jobs. |
1003 | + That is, unless a local job is explicitly requested (in the desired job |
1004 | + list) PlainBox will not be able to run a job that is generated by a local |
1005 | + job at all and will treat it as if that job never existed. |
1006 | + |
1007 | +* Some local jobs are used to create a form of informal "category". |
1008 | + Typically all such jobs have a leading and trailing double underscore, |
1009 | + for example '__audio__'. This is currently being used by CheckBox for |
1010 | + building a hierarchical tree of tests that the user may select. |
1011 | + |
1012 | + Since this has the same flaws as described above (for pattern jobs) it |
1013 | + will likely be replaced by an explicit category field that can be |
1014 | + specified each job. |
1015 | + |
1016 | +plugin == "resource" |
1017 | +#################### |
1018 | + |
1019 | +This value is used for special "data" or "environment" jobs. Their output is |
1020 | +parsed as a list of RFC822 records and is kept by the core during a testing session. |
1021 | + |
1022 | +They are primarily used to determine if a given job can be started. For |
1023 | +example, a particular bluetooth test may use the _requires_ field to indicate |
1024 | +that it depends (via a resource dependency) on a job that enumerates devices |
1025 | +and that one of those devices must be a bluetooth device. |
1026 | + |
1027 | +plugin == "user-interact" |
1028 | +######################### |
1029 | + |
1030 | +For all intents and purposes it is equivalent to "manual". The actual |
1031 | +difference is that a user is expected to perform some physical manipulation |
1032 | +before an automated test. |
1033 | + |
1034 | +plugin == "user-verify" |
1035 | +####################### |
1036 | + |
1037 | +For all intents and purposes it is equivalent to "manual". The actual |
1038 | +difference is that a user is expected to perform manual verification after an |
1039 | +automated test. |
1040 | + |
1041 | +JobDefinition.depends |
1042 | +--------------------- |
1043 | + |
1044 | +The *depends* field is used to express dependencies between two jobs. If job A |
1045 | +has depends on job B then A cannot start if B is not both finished and |
1046 | +successful. PlainBox understands this dependency and can automatically sort and |
1047 | +execute jobs in proper order. In many places of the code this is referred to as |
1048 | +a "direct dependency" (in contrast to "resource dependency") |
1049 | + |
1050 | +The actual syntax is not strictly specified, PlainBox interprets this field as |
1051 | +a list of tokens delimited by comma or any whitespace (including newlines). |
1052 | + |
1053 | +A job may depend on any number of other jobs. There are a number of failure |
1054 | +modes associated with this feature, all of which are detected and handled by |
1055 | +PlainBox. Typically they only arise when during CheckBox job development |
1056 | +(editing actual job files) and are always a sign of a human error. No released |
1057 | +version of CheckBox or PlainBox should ever encounter any of those issues. |
1058 | + |
1059 | +The actual problems are: |
1060 | + |
1061 | +* dependency cycles, where job either directly or indirectly depends on |
1062 | + itself |
1063 | + |
1064 | +* missing dependencies where some job refers to a job that is not defined |
1065 | + anywhere. |
1066 | + |
1067 | +* duplicate jobs where two jobs with the same name (but different |
1068 | + definition) are being introduced to the system. |
1069 | + |
1070 | +In all of those cases the core removes the offending job and tries to work |
1071 | +regardless of the problem. This is intended more as a development aid rather |
1072 | +than a reliability feature as no released versions of either project should |
1073 | +cause this problem. |
1074 | + |
1075 | +JobDefinition.command |
1076 | +--------------------- |
1077 | + |
1078 | +The *command* field is used when the job needs to call an external command. |
1079 | +Typically all shell jobs define a command to run. |
1080 | + |
1081 | +"Manual" jobs can also define a command to run as part of the test procedure. |
1082 | + |
1083 | +JobDefinition.user |
1084 | +------------------ |
1085 | + |
1086 | +The *user* field is used when the job requires to run as a specific user |
1087 | +(e.g. root). |
1088 | + |
1089 | +The job command will be run via pkexec to get the necessary |
1090 | +permissions. |
1091 | + |
1092 | +.. _environ: |
1093 | + |
1094 | +JobDefinition.environ |
1095 | +--------------------- |
1096 | + |
1097 | +The *environ* field is used to pass additional environmental keys from the user |
1098 | +session to the new environment set up when the job command is run by another |
1099 | +user (root, most of the time). |
1100 | + |
1101 | +The actual syntax is not strictly specified, PlainBox interprets this field as |
1102 | +a list of tokens delimited by comma or any whitespace (including newlines). |
1103 | |
1104 | === added file 'plainbox/docs/dev/reference.rst.OTHER' |
1105 | --- plainbox/docs/dev/reference.rst.OTHER 1970-01-01 00:00:00 +0000 |
1106 | +++ plainbox/docs/dev/reference.rst.OTHER 2013-08-28 12:37:27 +0000 |
1107 | @@ -0,0 +1,179 @@ |
1108 | +.. _code_reference: |
1109 | + |
1110 | +.. toctree:: |
1111 | + :maxdepth: 2 |
1112 | + |
1113 | +Code reference |
1114 | +============== |
1115 | + |
1116 | +.. note:: |
1117 | + |
1118 | + Unless stated otherwise all API is unstable. PlainBox does not offer |
1119 | + general API stability at this time. |
1120 | + |
1121 | +.. automodule:: plainbox |
1122 | + :members: |
1123 | + :undoc-members: |
1124 | + :show-inheritance: |
1125 | + |
1126 | +.. automodule:: plainbox.public |
1127 | + :members: |
1128 | + :undoc-members: |
1129 | + :show-inheritance: |
1130 | + |
1131 | +.. automodule:: plainbox.abc |
1132 | + :members: |
1133 | + :undoc-members: |
1134 | + :show-inheritance: |
1135 | + |
1136 | +.. automodule:: plainbox.tests |
1137 | + :members: |
1138 | + :undoc-members: |
1139 | + :show-inheritance: |
1140 | + |
1141 | +.. automodule:: plainbox.testing_utils |
1142 | + :members: |
1143 | + :undoc-members: |
1144 | + :show-inheritance: |
1145 | + |
1146 | +.. automodule:: plainbox.testing_utils.cwd |
1147 | + :members: |
1148 | + :undoc-members: |
1149 | + :show-inheritance: |
1150 | + |
1151 | +.. automodule:: plainbox.testing_utils.io |
1152 | + :members: |
1153 | + :undoc-members: |
1154 | + :show-inheritance: |
1155 | + |
1156 | +.. automodule:: plainbox.testing_utils.testcases |
1157 | + :members: |
1158 | + :undoc-members: |
1159 | + :show-inheritance: |
1160 | + |
1161 | +.. automodule:: plainbox.vendor |
1162 | + :members: |
1163 | + |
1164 | +.. automodule:: plainbox.vendor.extcmd |
1165 | + :members: |
1166 | + :undoc-members: |
1167 | + :show-inheritance: |
1168 | + |
1169 | +.. automodule:: plainbox.impl |
1170 | + :members: |
1171 | + :undoc-members: |
1172 | + :show-inheritance: |
1173 | + |
1174 | +.. automodule:: plainbox.impl.commands |
1175 | + :members: |
1176 | + :undoc-members: |
1177 | + :show-inheritance: |
1178 | + |
1179 | +.. automodule:: plainbox.impl.commands.selftest |
1180 | + :members: |
1181 | + :undoc-members: |
1182 | + :show-inheritance: |
1183 | + |
1184 | +.. automodule:: plainbox.impl.exporter |
1185 | + :members: |
1186 | + :undoc-members: |
1187 | + :show-inheritance: |
1188 | + |
1189 | +.. automodule:: plainbox.impl.exporter.json |
1190 | + :members: |
1191 | + :undoc-members: |
1192 | + :show-inheritance: |
1193 | + |
1194 | +.. automodule:: plainbox.impl.exporter.rfc822 |
1195 | + :members: |
1196 | + :undoc-members: |
1197 | + :show-inheritance: |
1198 | + |
1199 | +.. automodule:: plainbox.impl.exporter.text |
1200 | + :members: |
1201 | + :undoc-members: |
1202 | + :show-inheritance: |
1203 | + |
1204 | +.. automodule:: plainbox.impl.secure |
1205 | + :members: |
1206 | + :undoc-members: |
1207 | + :show-inheritance: |
1208 | + |
1209 | +.. automodule:: plainbox.impl.secure.checkbox_trusted_launcher |
1210 | + :members: |
1211 | + :undoc-members: |
1212 | + :show-inheritance: |
1213 | + |
1214 | +.. automodule:: plainbox.impl.transport |
1215 | + :members: |
1216 | + :undoc-members: |
1217 | + :show-inheritance: |
1218 | + |
1219 | +.. automodule:: plainbox.impl.transport.certification |
1220 | + :members: |
1221 | + :undoc-members: |
1222 | + :show-inheritance: |
1223 | + |
1224 | +.. automodule:: plainbox.impl.box |
1225 | + :members: |
1226 | + :undoc-members: |
1227 | + :show-inheritance: |
1228 | + |
1229 | +.. automodule:: plainbox.impl.checkbox |
1230 | + :members: |
1231 | + :undoc-members: |
1232 | + :show-inheritance: |
1233 | + |
1234 | +.. automodule:: plainbox.impl.config |
1235 | + :members: |
1236 | + :undoc-members: |
1237 | + :show-inheritance: |
1238 | + |
1239 | +.. automodule:: plainbox.impl.depmgr |
1240 | + :members: |
1241 | + :undoc-members: |
1242 | + :show-inheritance: |
1243 | + |
1244 | +.. automodule:: plainbox.impl.integration_tests |
1245 | + :members: |
1246 | + :undoc-members: |
1247 | + :show-inheritance: |
1248 | + |
1249 | +.. automodule:: plainbox.impl.job |
1250 | + :members: |
1251 | + :undoc-members: |
1252 | + :show-inheritance: |
1253 | + |
1254 | +.. automodule:: plainbox.impl.mock_job |
1255 | + :members: |
1256 | + :undoc-members: |
1257 | + :show-inheritance: |
1258 | + |
1259 | +.. automodule:: plainbox.impl.resource |
1260 | + :members: |
1261 | + :undoc-members: |
1262 | + :show-inheritance: |
1263 | + |
1264 | +.. automodule:: plainbox.impl.result |
1265 | + :members: |
1266 | + :undoc-members: |
1267 | + :show-inheritance: |
1268 | + |
1269 | +.. automodule:: plainbox.impl.rfc822 |
1270 | + :members: |
1271 | + :show-inheritance: |
1272 | + |
1273 | +.. automodule:: plainbox.impl.runner |
1274 | + :members: |
1275 | + :undoc-members: |
1276 | + :show-inheritance: |
1277 | + |
1278 | +.. automodule:: plainbox.impl.session |
1279 | + :members: |
1280 | + :undoc-members: |
1281 | + :show-inheritance: |
1282 | + |
1283 | +.. automodule:: plainbox.impl.testing_utils |
1284 | + :members: |
1285 | + :undoc-members: |
1286 | + :show-inheritance: |
1287 | |
1288 | === added file 'plainbox/docs/dev/resources.rst.OTHER' |
1289 | --- plainbox/docs/dev/resources.rst.OTHER 1970-01-01 00:00:00 +0000 |
1290 | +++ plainbox/docs/dev/resources.rst.OTHER 2013-08-28 12:37:27 +0000 |
1291 | @@ -0,0 +1,259 @@ |
1292 | +Resources |
1293 | +========= |
1294 | + |
1295 | +Resources are a mechanism that allows to constrain certain :term:`job` to |
1296 | +execute only on devices with appropriate hardware or software dependencies. |
1297 | +This mechanism allows some types of jobs to publish resource objects to an |
1298 | +abstract namespace and to a way to evaluate a resource program to determine if |
1299 | +a job can be started. |
1300 | + |
1301 | +Resources in PlainBox |
1302 | +===================== |
1303 | + |
1304 | +The following chapters explain how resources actually work in :term:`PlainBox`. |
1305 | +Currently there *is* a subtle difference between this and the original |
1306 | +:term:`CheckBox` implementation. |
1307 | + |
1308 | +Resource programs |
1309 | +----------------- |
1310 | + |
1311 | +Resource programs are multi-line statements that can be embedded in job |
1312 | +definitions. By far, the most common use case is to check if a required package |
1313 | +is installed, and thus, the job can use it as a part of a test. A check like |
1314 | +this looks like this:: |
1315 | + |
1316 | + package.name == "fwts" |
1317 | + |
1318 | +This resource program codifies that the job needs the ``fwts`` package to run. |
1319 | +There is a companion job with the same name that interrogates the local package |
1320 | +database and publishes a set of resource objects. Each such object is a |
1321 | +collection of arbitrary key-value pairs. The ``package`` job simply publishes |
1322 | +the ``name`` and ``version`` of each installed package but the mechanism is |
1323 | +generic and applies to all resources. |
1324 | + |
1325 | +As stated, resource programs can be multi-line, a real world example of that is |
1326 | +presented below:: |
1327 | + |
1328 | + device.category == 'CDROM' |
1329 | + optical_drive.cd == 'writable' |
1330 | + |
1331 | +This example is much like the one above, referring to some resources, here |
1332 | +coming from jobs ``device`` and ``optical_drive``. What is important to point |
1333 | +out is that, as a rule of a thumb, multi line programs have an implicit ``and`` |
1334 | +operator between each line. This program would only evaluate to True if there |
1335 | +is a writable CD-ROM available. |
1336 | + |
1337 | +Each resource program is composed of resource expressions. Each line maps |
1338 | +directly onto one expression so the example program above uses two resource |
1339 | +expressions. |
1340 | + |
1341 | +Resource expressions |
1342 | +-------------------- |
1343 | + |
1344 | +Resource expressions are evaluated like normal python programs. They use all of |
1345 | +the same syntax, semantics and behavior. None of the operators are overridden |
1346 | +to do anything unexpected. The evaluator tries to follow the principle of least |
1347 | +surprise but this is not always possible. |
1348 | + |
1349 | +Resource expressions cannot execute arbitrary python code. In general almost |
1350 | +everything is disallowed, except as noted below: |
1351 | + |
1352 | +* Expressions can use any literals (strings, numbers, True, False, lists and tuples) |
1353 | +* Expressions can use boolean operators (``and``, ``or``, ``not``) |
1354 | +* Expressions can use all comparison operators |
1355 | +* Expressions can use all binary and unary operators |
1356 | +* Expressions can use the set membership operator (``in``) |
1357 | +* Expressions can use read-only attribute access |
1358 | + |
1359 | +Anything else is rejected as an invalid resource expression. |
1360 | + |
1361 | +In addition to that, each resource expression must use exactly one variable, |
1362 | +which must be used like an object with attributes. The name of that variable |
1363 | +must correspond to the name of the job that generates resources. Attempts to |
1364 | +use more than one variable or to not use any variables are detected early and |
1365 | +rejected as invalid resource expressions. |
1366 | + |
1367 | +The name of the variable determines which resource group to use. It must match |
1368 | +the name of the job that generates such resources. |
1369 | + |
1370 | +In the examples elsewhere in this page the ``package`` resources are generated |
1371 | +by the ``package`` job. PlainBox uses this to know which resources to try but |
1372 | +also to implicitly to express dependencies so that the ``package`` job does not |
1373 | +have to be explicitly selected and marked for execution prior to the job that |
1374 | +in fact depends on it. This is all done automatically. |
1375 | + |
1376 | +Evaluation |
1377 | +---------- |
1378 | + |
1379 | +Due to mandatory compatibility with existing :term:`CheckBox` jobs there are |
1380 | +some unexpected aspects of how evaluation is performed. Those are marked as |
1381 | +**unexpected** below: |
1382 | + |
1383 | +1. First PlainBox looks at the resource program and splits it into lines. Each |
1384 | + non-empty line is parsed and converted to a resource expression. |
1385 | + |
1386 | +2. **unexpected** Each resource expression is repeatedly evaluated, once for |
1387 | + each resource from the group determined by the variable name. All exceptions |
1388 | + are silently ignored and treated as if the iteration had evaluated to False. |
1389 | + The whole resource expression evaluates to ``True`` if any of the iterations |
1390 | + evaluated to ``True``. In other words, there is an implicit ``any()`` around |
1391 | + each resource expression, iterating over all resources. |
1392 | + |
1393 | +3. **unexpected** The resource program evaluates to ``True`` only if all |
1394 | + resource expressions evaluated to ``True``. In other words, there is an |
1395 | + implicit ``and`` between each line. |
1396 | + |
1397 | +Limitations |
1398 | +----------- |
1399 | + |
1400 | +The design of resource programs has the following shortcomings. The list is |
1401 | +non-exhaustive, it only contains issues that we came across found not to work |
1402 | +in practice. |
1403 | + |
1404 | +Exactly one variable per expression |
1405 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1406 | + |
1407 | +Each resource expression must refer to exactly one variable. This is a side |
1408 | +effect of the way the evaluator works. It basically bind one object (a |
1409 | +particular resource) to that variable and evaluates the expression. |
1410 | + |
1411 | +The expression parser / syntax analyzer identifies expressions with this |
1412 | +problem early and rejects them with an appropriate error message. Here are |
1413 | +some examples of hypothetical expressions that exhibit this problem. |
1414 | + |
1415 | +"I want to have mplayer and an audio device so that I can play some sounds":: |
1416 | + |
1417 | + device.category == "AUDIO" and package.name == "mplayer" |
1418 | + |
1419 | +To work around this, split the expression to two separate expressions. The |
1420 | +evaluator will put an implicit ``and`` between them and it will do exactly what |
1421 | +you intended:: |
1422 | + |
1423 | + device.category == "AUDIO" |
1424 | + package.name == "mplayer" |
1425 | + |
1426 | +"I want to always run this test":: |
1427 | + |
1428 | + True |
1429 | + |
1430 | +To work around this, simply remove the requirement program entirely! |
1431 | + |
1432 | +"I want to never run this test":: |
1433 | + |
1434 | + False |
1435 | + |
1436 | +To work around this remove this job from the selection. You may also use a |
1437 | +special resource that produces one constant value, and check that it is equal |
1438 | +to something different. |
1439 | + |
1440 | +Exactly one resource bound to a variable at once |
1441 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1442 | + |
1443 | +It's not possible to refer to two different resources, from the same resource |
1444 | +group, in one resource expression. In other terms, the variable always points |
1445 | +to one object, it is not a collection of objects. |
1446 | + |
1447 | +For example, let's consider this program:: |
1448 | + |
1449 | + package.name == 'xorg' and package.name == 'procps' |
1450 | + |
1451 | +Seemingly the intent was to ensure that both ``xorg`` and ``procps`` are |
1452 | +installed. The reason why this does not work is that at each iteration of the |
1453 | +the expression evaluator, the name ``package`` refers to exactly one resource |
1454 | +object. In other words, that expression is equivalent to this one:: |
1455 | + |
1456 | + A == True and A == False |
1457 | + |
1458 | +This type of error is not captured by our limited semantic analyzer. It will |
1459 | +silently evaluate to False and inhibit the job from being stated. |
1460 | + |
1461 | +To work around this, split the expression to two consecutive lines. As stated |
1462 | +in rule 3 in the list above, there is an implicit ``and`` operator between all |
1463 | +expressions. A working example that expresses the same intent looks like this:: |
1464 | + |
1465 | + package.name == 'xorg' |
1466 | + package.name == 'procps' |
1467 | + |
1468 | +Operator != is useless |
1469 | +^^^^^^^^^^^^^^^^^^^^^^ |
1470 | + |
1471 | +This is strange at first but quickly becomes obvious once you recall rule 2 |
1472 | +from the list above. That rule states that the expression is evaluated |
1473 | +repeatedly for each resource from a particular group and that any ``True`` |
1474 | +iteration marks the whole expression as ``True``). |
1475 | + |
1476 | +Let's look at a real-world example:: |
1477 | + |
1478 | + xinput.device_class == 'XITouchClass' and xinput.touch_mode != 'dependent' |
1479 | + |
1480 | +So seemingly, the intent here was to have at least ``xinput`` resource with a |
1481 | +``device_class`` attribute equal to ``XITouchClass`` that has ``touch_mode`` |
1482 | +attribute equal to anything but ``dependent``. |
1483 | + |
1484 | +Now let's assume that we have exactly two resources in the ``xinput`` group:: |
1485 | + |
1486 | + device_class: XITouchClass |
1487 | + touch_mode: dependant |
1488 | + |
1489 | + device_class: XITouchClass |
1490 | + touch_mode: something else |
1491 | + |
1492 | +Now, this expression will evaluate to ``True``, as the second resource fulfils |
1493 | +the requirements. Is this what the test designer had expected? That's hard to |
1494 | +say. The problem here is that this expression can be understood as *at least |
1495 | +one resource isn't something* **or** *all resources weren't something*. Both |
1496 | +are equally valid desires and, depending on how the test is implemented, may or |
1497 | +many not work correctly in practice. |
1498 | + |
1499 | +Currently there is no workaround. We are considering adding a new syntax that |
1500 | +would allow to specify this explicitly. The proposal is documented below as |
1501 | +"implicit any(), explicit all()" |
1502 | + |
1503 | +Everything is a string |
1504 | +^^^^^^^^^^^^^^^^^^^^^^ |
1505 | + |
1506 | +Resource programs are regular python programs evaluated in unusual ways but |
1507 | +all of the variables that are exposed through the resource object are strings. |
1508 | + |
1509 | +This has considerable impact on comparison, unless you are comparing to a |
1510 | +string the comparison will always silently fail as python has dynamic but |
1511 | +strict, not loose types (there is no implicit type conversion). To alleviate |
1512 | +this problem several type names / conversion functions are allowed in |
1513 | +requirement programs. Those are: |
1514 | + |
1515 | +* :py:class:`int`, to convert to integer numbers |
1516 | +* :py:class:`float`, to convert to floating point numbers |
1517 | +* :py:class:`bool`, to convert to a boolean context |
1518 | + |
1519 | +Considered enhancements |
1520 | +----------------------- |
1521 | + |
1522 | +We are currently considering one improvement to resource programs. This would |
1523 | +allow us to introduce a fix that resolves some issues in a backwards compatible |
1524 | +way. Technical aspects are not yet resolved as that extension would not be |
1525 | +available in :term:`CheckBox` until CheckBox can be built on top of |
1526 | +:term:`PlainBox` |
1527 | + |
1528 | +Implicit any(), explicit all() |
1529 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1530 | + |
1531 | +This proposal changes the way resource expressions are evaluated. |
1532 | + |
1533 | +The implicit ``any()`` implemented as a loop over all resources from the |
1534 | +resource group designated by variable name would be configurable. |
1535 | + |
1536 | +A developer may choose to wrap the whole expression in the ``all()`` function |
1537 | +to indicate that the expression inside ``all()`` must evaluate to ``True`` for |
1538 | +**all** iterations (all resources). |
1539 | + |
1540 | +This would allow solving the case where a job can only run, for example, when a |
1541 | +certain package is **not** installed. This could be expressed as:: |
1542 | + |
1543 | + all(package.name != 'ubuntu-desktop') |
1544 | + |
1545 | +Resources in CheckBox |
1546 | +===================== |
1547 | + |
1548 | +The following chapters explain how resources originally worked in |
1549 | +:term:`CheckBox`. Only notable differences from :term:`PlainBox` implementation |
1550 | +are listed. |
1551 | |
1552 | === added file 'plainbox/docs/dev/trusted-launcher.rst' |
1553 | --- plainbox/docs/dev/trusted-launcher.rst 1970-01-01 00:00:00 +0000 |
1554 | +++ plainbox/docs/dev/trusted-launcher.rst 2013-08-28 12:37:27 +0000 |
1555 | @@ -0,0 +1,209 @@ |
1556 | +Running jobs as root |
1557 | +==================== |
1558 | + |
1559 | +:term:`PlainBox` is started without any privilege. |
1560 | +But several tests need to start commands requiring privileges. |
1561 | + |
1562 | +Such tests will call a trusted launcher, a standalone script |
1563 | +which does not depend on the :term:`PlainBox` core modules. |
1564 | +`polkit <http://www.freedesktop.org/wiki/Software/polkit>`_ |
1565 | +will control access to system resources. |
1566 | +The trusted launcher has to be started using |
1567 | +`pkexec <http://www.freedesktop.org/software/polkit/docs/0.105/pkexec.1.html>`_ |
1568 | +so that the related policy file works as expected. |
1569 | + |
1570 | +To avoid a security hole that allows anyone to run anything as root, |
1571 | +the launcher can only run jobs installed in a system-wide directory. |
1572 | +This way we are not weaken the trust system as root access is required |
1573 | +to install both components (the trusted runner and jobs). |
1574 | +The :term:`PlainBox` process will send an identifier which is matched by a well-known |
1575 | +list in the trusted launcher. This identifier is the job hash: |
1576 | + |
1577 | +.. code-block:: bash |
1578 | + |
1579 | + $ pkexec trusted-launcher JOB-HASH |
1580 | + |
1581 | +See :meth:`plainbox.impl.secure.checkbox_trusted_launcher.BaseJob.get_checksum()` for details about job hashes. |
1582 | + |
1583 | +Using Polkit |
1584 | +^^^^^^^^^^^^ |
1585 | + |
1586 | +Available authentication methods |
1587 | +-------------------------------- |
1588 | + |
1589 | +.. note:: |
1590 | + |
1591 | + Only applicable to the package version of PlainBox |
1592 | + |
1593 | +PlainBox comes with two authentication methods but both aim to retain the |
1594 | +granted privileges for the life of the :term:`PlainBox` process. |
1595 | + |
1596 | +* The first method will ask the password only once and show the following |
1597 | + agent on desktop systems (a text-based agent is available for servers): |
1598 | + |
1599 | + .. code-block:: text |
1600 | + |
1601 | + +-----------------------------------------------------------------------------+ |
1602 | + | [X] Authenticate | |
1603 | + +-----------------------------------------------------------------------------+ |
1604 | + | | |
1605 | + | [Icon] Please enter your password. Some tests require root access to run | |
1606 | + | properly. Your password will never be stored and will never be | |
1607 | + | submitted with test results. | |
1608 | + | | |
1609 | + | An application is attempting to perform an action that requires | |
1610 | + | privileges. | |
1611 | + | Authentication as the super user is required to perform this action. | |
1612 | + | | |
1613 | + | Password: [________________________________________________________] | |
1614 | + | | |
1615 | + | [V] Details: | |
1616 | + | Action: org.freedesktop.policykit.pkexec.run-plainbox-job | |
1617 | + | Vendor: PlainBox | |
1618 | + | | |
1619 | + | [Cancel] [Authenticate] | |
1620 | + +-----------------------------------------------------------------------------+ |
1621 | + |
1622 | + The following policy file has to be installed in :file:`/usr/share/polkit-1/actions/` |
1623 | + on Ubuntu systems. |
1624 | + Asking the password just one time and keeps the authentication for forthcoming |
1625 | + calls is provided by the **allow_active** element and the **auth_admin_keep** value. |
1626 | + |
1627 | + Check the `polkit actions <http://www.freedesktop.org/software/polkit/docs/0.105/polkit.8.html#polkit-declaring-actions>`_ |
1628 | + documentation for details about the other parameters. |
1629 | + |
1630 | + .. code-block:: xml |
1631 | + |
1632 | + <?xml version="1.0" encoding="UTF-8"?> |
1633 | + <!DOCTYPE policyconfig PUBLIC |
1634 | + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" |
1635 | + "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> |
1636 | + <policyconfig> |
1637 | + |
1638 | + <vendor>PlainBox</vendor> |
1639 | + <vendor_url>https://launchpad.net/checkbox</vendor_url> |
1640 | + <icon_name>checkbox</icon_name> |
1641 | + |
1642 | + <action id="org.freedesktop.policykit.pkexec.run-plainbox-job"> |
1643 | + <description>Run Job command</description> |
1644 | + <message>Authentication is required to run a job command.</message> |
1645 | + <defaults> |
1646 | + <allow_any>no</allow_any> |
1647 | + <allow_inactive>no</allow_inactive> |
1648 | + <allow_active>auth_admin_keep</allow_active> |
1649 | + </defaults> |
1650 | + <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/checkbox-trusted-launcher</annotate> |
1651 | + <annotate key="org.freedesktop.policykit.exec.allow_gui">TRUE</annotate> |
1652 | + </action> |
1653 | + |
1654 | + </policyconfig> |
1655 | + |
1656 | +* The second method is only intended to be used in headless mode (like `SRU`). |
1657 | + The only difference with the above method is that **allow_active** will be set to **yes**. |
1658 | + |
1659 | +.. note:: |
1660 | + |
1661 | + The two policy files are available in the PlainBox :file:`contrib/` directory. |
1662 | + |
1663 | +Environment settings with pkexec |
1664 | +-------------------------------- |
1665 | + |
1666 | +`pkexec <http://www.freedesktop.org/software/polkit/docs/0.105/pkexec.1.html>`_ |
1667 | +allows an authorized user to execute a command as another user. |
1668 | +But the environment that ``command`` will run it, will be set to a minimal known |
1669 | +and safe environment in order to avoid injecting code through ``LD_LIBRARY_PATH`` |
1670 | +or similar mechanisms. |
1671 | + |
1672 | +However, some jobs commands require specific enviroment variables such as the |
1673 | +name of an access point for a wireless test. Those kind of variables must be |
1674 | +available to the trusted launcher. |
1675 | +To do so, the enviromment mapping is sent to the launcher like key/value pairs |
1676 | +are sent to the env(1) command: |
1677 | + |
1678 | +.. code-block:: bash |
1679 | + |
1680 | + $ pkexec trusted-launcher JOB-HASH [NAME=VALUE [NAME=VALUE ...]] |
1681 | + |
1682 | +Each NAME will be set to VALUE in the environment given that they are known |
1683 | +and defined in the :ref:`JobDefinition.environ <environ>` parameter. |
1684 | + |
1685 | +Checkbox trusted launcher |
1686 | +^^^^^^^^^^^^^^^^^^^^^^^^^ |
1687 | + |
1688 | +The checkbox trusted launcher is the minimal code needed to be able to run a |
1689 | +:term:`CheckBox` job command. |
1690 | + |
1691 | +It offers base classes for the following core subclasses: |
1692 | + |
1693 | +* :class:`plainbox.impl.rfc822.RFC822Record` |
1694 | +* :class:`plainbox.impl.job.JobDefinition` |
1695 | + |
1696 | +The only duplicated code is the RFC822 parser, where all logging features have |
1697 | +been removed. |
1698 | + |
1699 | +The :class:`plainbox.impl.secure.checkbox_trusted_launcher.Runner` class just |
1700 | +executes the command process with :py:func:`os.execve`. |
1701 | + |
1702 | +Internally the checkbox trusted launcher looks for jobs in the system locations defined in |
1703 | +:attr:`plainbox.impl.secure.checkbox_trusted_launcher.Runner.CHECKBOXES` which defaults to :file:`/usr/share/checkbox*`. |
1704 | +This way the launcher can match all :term:`CheckBox` variants, like ``checkbox-oem(-.*)?`` |
1705 | + |
1706 | +Usage |
1707 | +----- |
1708 | + |
1709 | +.. code-block:: text |
1710 | + |
1711 | + checkbox-trusted-launcher [-h] (--hash HASH | --warmup) |
1712 | + [--via LOCAL-JOB-HASH] |
1713 | + [NAME=VALUE [NAME=VALUE ...]] |
1714 | + |
1715 | + positional arguments: |
1716 | + NAME=VALUE Set each NAME to VALUE in the string environment |
1717 | + |
1718 | + optional arguments: |
1719 | + -h, --help show this help message and exit |
1720 | + --hash HASH job hash to match |
1721 | + --warmup Return immediately, only useful when used with |
1722 | + pkexec(1) |
1723 | + --via LOCAL-JOB-HASH Local job hash to use to match the generated job |
1724 | + |
1725 | +.. note:: |
1726 | + |
1727 | + Check all job hashes with ``plainbox special -J`` |
1728 | + |
1729 | +As stated in the polkit chapter, only a trusted subset of the environment mapping |
1730 | +will be set using :py:func:`os.execve` to run the command. |
1731 | +Only the variables defined in the job environ property are allowed to avoid |
1732 | +compromising the root environment. |
1733 | +Needed modifications like adding ``CHECKBOX_SHARE`` and new paths to scripts are |
1734 | +managed by the checkbox-trusted-launcher. |
1735 | + |
1736 | +Authentication on PlainBox startup |
1737 | +---------------------------------- |
1738 | + |
1739 | +To avoid prompting the password at the first test requiring privileges, :term:`PlainBox` |
1740 | +will call the ``checkbox-trusted-launcher`` with the ``--warmup`` option. |
1741 | +It's like a NOOP and it will return immediately, but thanks to the installed policy file |
1742 | +the authentication will be kept. |
1743 | + |
1744 | +.. note:: |
1745 | + |
1746 | + When running the development version from a branch, the usual polkit |
1747 | + authentication agent will pop up to ask the password each and every time. |
1748 | + This is the only difference. |
1749 | + |
1750 | +Special case of jobs using the CheckBox local plugin |
1751 | +---------------------------------------------------- |
1752 | + |
1753 | +For jobs generated from :ref:`local <local>` jobs (e.g. disk/read_performance.*) |
1754 | +the trusted launcher is started with ``--via`` meaning that we have to first |
1755 | +eval a local job to find a hash match. |
1756 | +Once a match is found, the job command is executed using :py:func:`os.execve`. |
1757 | + |
1758 | +.. code-block:: bash |
1759 | + |
1760 | + $ pkexec checkbox-trusted-launcher --hash JOB-HASH --via LOCAL-JOB-HASH |
1761 | + |
1762 | +.. note:: |
1763 | + |
1764 | + it will obviously fail if any local job can ever generate another local job. |
1765 | |
1766 | === added file 'plainbox/docs/usage.rst.OTHER' |
1767 | --- plainbox/docs/usage.rst.OTHER 1970-01-01 00:00:00 +0000 |
1768 | +++ plainbox/docs/usage.rst.OTHER 2013-08-28 12:37:27 +0000 |
1769 | @@ -0,0 +1,93 @@ |
1770 | +.. _usage: |
1771 | + |
1772 | +Usage |
1773 | +===== |
1774 | + |
1775 | +Currently :term:`PlainBox` has no graphical user interface. To use it you need |
1776 | +to use the command line. |
1777 | + |
1778 | +Basically there is just one command that does everything we can do so far, that |
1779 | +is :command:`plainbox run`. It has a number of options that tell it which |
1780 | +:term:`job` to run and what to do with results. |
1781 | + |
1782 | +PlainBox has built-in help system so running :command:`plainbox run --help` |
1783 | +will give you instant information about all the various arguments and options |
1784 | +that are available. This document is not intended to replace that. |
1785 | + |
1786 | +Running a specific job |
1787 | +^^^^^^^^^^^^^^^^^^^^^^ |
1788 | + |
1789 | +To run a specific :term:`job` pass it to the ``--include-pattern`` or ``-i`` |
1790 | +option. |
1791 | + |
1792 | +For example, to run the ``cpu/scaling_test`` job: |
1793 | + |
1794 | +.. code-block:: bash |
1795 | + |
1796 | + $ plainbox run -i cpu/scaling_test |
1797 | + |
1798 | +.. note:: |
1799 | + |
1800 | + The option ``-i`` can be provided any number of times. |
1801 | + |
1802 | +Running jobs related to a specific area |
1803 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1804 | + |
1805 | +PlainBox has no concept of job categories but you can simulate that by |
1806 | +running all jobs that follow a specific naming pattern. For example, to run |
1807 | +all of the USB tests you can run the following command: |
1808 | + |
1809 | +.. code-block:: bash |
1810 | + |
1811 | + $ plainbox run -i 'usb/.*' |
1812 | + |
1813 | +To list all known jobs run: |
1814 | + |
1815 | +.. code-block:: bash |
1816 | + |
1817 | + plainbox special --list-jobs |
1818 | + |
1819 | +Running a white list |
1820 | +^^^^^^^^^^^^^^^^^^^^ |
1821 | + |
1822 | +To run a :term:`white list` pass the ``--whitelist`` or ``-w`` option. |
1823 | + |
1824 | +For example, to run the default white list run: |
1825 | + |
1826 | +.. code-block:: bash |
1827 | + |
1828 | + $ plainbox run -w /usr/share/checkbox/data/whitelists/default.whitelist |
1829 | + |
1830 | +Saving test results as XML |
1831 | +^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1832 | + |
1833 | +To generate an XML file that can be sent to the :term:`certification website` |
1834 | +you need to pass two additional options: |
1835 | + |
1836 | +1. ``--output-format=xml`` |
1837 | +2. ``--output-file=NAME`` where *NAME* is a file name |
1838 | + |
1839 | +For example, to get the default certification tests ready to be submitted |
1840 | +run this command: |
1841 | + |
1842 | +.. code-block:: bash |
1843 | + |
1844 | + $ plainbox run --whitelist=/usr/share/checkbox/data/whitelists/default.whitelist --output-format=xml --output-file=submission.xml |
1845 | + |
1846 | +Running stable release update tests |
1847 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
1848 | + |
1849 | +PlainBox has special support for running stable release updates tests in an |
1850 | +automated manner. This runs all the jobs from the *sru.whitelist* and sends the |
1851 | +results to the certification website. |
1852 | + |
1853 | +To run SRU tests you will need to know the so-called :term:`Secure ID` of the |
1854 | +device you are testing. Once you know that all you need to do is run: |
1855 | + |
1856 | +.. code-block:: bash |
1857 | + |
1858 | + $ plainbox sru $secure_id submission.xml |
1859 | + |
1860 | +The second argument, submission.xml, is a name of the fallback file that is |
1861 | +only created when sending the data to the certification website fails to work |
1862 | +for any reason. |
1863 | |
1864 | === added file 'plainbox/mk-venv.sh.OTHER' |
1865 | --- plainbox/mk-venv.sh.OTHER 1970-01-01 00:00:00 +0000 |
1866 | +++ plainbox/mk-venv.sh.OTHER 2013-08-28 12:37:27 +0000 |
1867 | @@ -0,0 +1,195 @@ |
1868 | +#!/bin/sh |
1869 | +# Create a virtualenv for working with plainbox. |
1870 | +# |
1871 | +# This ensures that 'plainbox' command exists and is in PATH and that the |
1872 | +# plainbox module is correctly located can be imported. |
1873 | +# |
1874 | +# This is how Zygmunt Krynicki works, feel free to use or adjust to your needs |
1875 | + |
1876 | +VENV_PATH= |
1877 | +install_missing=0 |
1878 | +# Parse arguments: |
1879 | +while [ -n "$1" ]; do |
1880 | + case "$1" in |
1881 | + --help|-h) |
1882 | + echo "Usage: mk-venv.sh [LOCATION]" |
1883 | + echo "" |
1884 | + echo "Create a virtualenv for working with plainbox in LOCATION" |
1885 | + exit 0 |
1886 | + ;; |
1887 | + --install-missing) |
1888 | + install_missing=1 |
1889 | + shift |
1890 | + ;; |
1891 | + *) |
1892 | + if [ -z "$VENV_PATH" ]; then |
1893 | + VENV_PATH="$1" |
1894 | + shift |
1895 | + else |
1896 | + echo "Error: too many arguments: '$1'" |
1897 | + exit 1 |
1898 | + fi |
1899 | + ;; |
1900 | + esac |
1901 | +done |
1902 | + |
1903 | +# Apply defaults to arguments without values |
1904 | +if [ -z "$VENV_PATH" ]; then |
1905 | + # Use sensible defaults for vagrant |
1906 | + if [ "$LOGNAME" = "vagrant" ]; then |
1907 | + VENV_PATH=/tmp/venv |
1908 | + else |
1909 | + VENV_PATH=/ramdisk/venv |
1910 | + fi |
1911 | +fi |
1912 | + |
1913 | +# Do a sanity check on lsb_release that is missing on Fedora the last time I |
1914 | +# had a look at it. |
1915 | +if [ "x$(which lsb_release)" = "x" ]; then |
1916 | + echo "This script requires the 'lsb_release' command" |
1917 | + exit 1 |
1918 | +fi |
1919 | + |
1920 | +# The code below is a mixture of Debian/Ubuntu packages and pypi packages. |
1921 | +# It is designed to work on Ubuntu 12.04 or later. |
1922 | +# There are _some_ differences between how each release is handled. |
1923 | +# |
1924 | +# Non Ubuntu systems are not tested as they don't have the required checkbox |
1925 | +# package. Debian might be supported once we have JobBox and stuff like Fedora |
1926 | +# would need a whole new approach but patches are welcome [CLA required] |
1927 | +if [ "$(lsb_release --short --id)" != "Ubuntu" ] && [ $(lsb_release --short --id --upstream) != "Ubuntu" ]; then |
1928 | + echo "Only Ubuntu is supported by this script." |
1929 | + echo "If you are interested in using it with your distribution" |
1930 | + echo "then please join us in #ubuntu-quality on freenode" |
1931 | + echo |
1932 | + echo "Alternatively you can use vagrant to develop plainbox" |
1933 | + echo "on any operating system, even Windows ;-)" |
1934 | + echo |
1935 | + echo "See: http://www.vagrantup.com/ for details" |
1936 | + exit 1 |
1937 | +fi |
1938 | +# From now on we can assume a Debian-like system |
1939 | + |
1940 | +# Do some conditional stuff depending on the particular Ubuntu release |
1941 | +enable_system_site=0 |
1942 | +install_coverage=0 |
1943 | +install_distribute=0 |
1944 | +install_pip=0 |
1945 | +# We need: |
1946 | +# python3: |
1947 | +# because that's what plainbox is written in |
1948 | +# python3-dev |
1949 | +# because we may pip-install stuff as well and we want to build native extensions |
1950 | +# python3-pkg-resources: |
1951 | +# because it is used by plainbox to locate files and extension points |
1952 | +# python3-setuptools: |
1953 | +# because it is used by setup.py |
1954 | +# python3-lxml: |
1955 | +# because that's how we validate RealaxNG schemas |
1956 | +# python3-mock: |
1957 | +# because that's what we used to construct some of our tests |
1958 | +# python3-sphinx: |
1959 | +# because that's how we build our documentation |
1960 | +# python-virtualenv: |
1961 | +# because that's how we create the virtualenv to work in |
1962 | +# checkbox: |
1963 | +# because plainbox depends on it as a job provider |
1964 | +required_pkgs_base="python3 python3-dev python3-pkg-resources python3-setuptools python3-lxml python3-mock python3-sphinx python-virtualenv checkbox" |
1965 | + |
1966 | +# The defaults, install everything from pip and all the base packages |
1967 | +enable_system_site=1 |
1968 | +install_distribute=1 |
1969 | +install_pip=1 |
1970 | +install_coverage=1 |
1971 | +install_requests=1 |
1972 | +required_pkgs="$required_pkgs_base" |
1973 | + |
1974 | +case "$(lsb_release --short --release)" in |
1975 | + 12.04|0.2) |
1976 | + # Ubuntu 12.04, this is the LTS release that we have to support despite |
1977 | + # any difficulties. It has python3.2 and all of our core dependencies |
1978 | + # although some packages are old by 13.04 standards, make sure to be |
1979 | + # careful with testing against older APIs. |
1980 | + ;; |
1981 | + 12.10) |
1982 | + ;; |
1983 | + 13.04) |
1984 | + # On Raring we can use the system package for python3-requests |
1985 | + install_distribute=0 |
1986 | + install_pip=0 |
1987 | + install_requests=0 |
1988 | + required_pkgs="$required_pkgs_base python3-requests" |
1989 | + ;; |
1990 | + *) |
1991 | + echo "Using this version of Ubuntu for development is not supported" |
1992 | + echo "Unsupported version: $(lsb_release --short --release)" |
1993 | + exit 1 |
1994 | + ;; |
1995 | +esac |
1996 | + |
1997 | +# Check if we can create a virtualenv |
1998 | +if [ ! -d $(dirname $VENV_PATH) ]; then |
1999 | + echo "This script requires $(dirname $VENV_PATH) directory to exist" |
2000 | + echo "You can use different directory by passing it as argument" |
2001 | + echo "For a quick temporary location just pass /tmp/venv" |
2002 | + exit 1 |
2003 | +fi |
2004 | + |
2005 | +# Check if there's one already there |
2006 | +if [ -d $VENV_PATH ]; then |
2007 | + echo "$VENV_PATH seems to already exist" |
2008 | + exit 1 |
2009 | +fi |
2010 | + |
2011 | +# Ensure that each required package is installed |
2012 | +for pkg_name in $required_pkgs; do |
2013 | + # Ensure virtualenv is installed |
2014 | + if [ "$(dpkg -s $pkg_name 2>/dev/null | grep '^Status' 2>/dev/null)" != "Status: install ok installed" ]; then |
2015 | + if [ "$install_missing" -eq 1 ]; then |
2016 | + echo "Installing required package: $pkg_name" |
2017 | + sudo apt-get install $pkg_name --yes |
2018 | + else |
2019 | + echo "Required package is not installed: '$pkg_name'" |
2020 | + echo "Either install it manually with:" |
2021 | + echo "$ sudo apt-get install $pkg_name" |
2022 | + echo "Or rerun this script with --install-missing" |
2023 | + exit 1 |
2024 | + fi |
2025 | + fi |
2026 | +done |
2027 | + |
2028 | +# Create a virtualenv |
2029 | +if [ $enable_system_site -eq 1 ]; then |
2030 | + virtualenv --system-site-packages -p python3 $VENV_PATH |
2031 | +else |
2032 | + virtualenv -p python3 $VENV_PATH |
2033 | +fi |
2034 | + |
2035 | +# Activate it to install additional stuff |
2036 | +. $VENV_PATH/bin/activate |
2037 | + |
2038 | +# Install / upgrade distribute |
2039 | +if [ $install_distribute -eq 1 ]; then |
2040 | + pip install --upgrade https://github.com/checkbox/external-tarballs/raw/master/pypi/coverage-3.6.tar.gz |
2041 | +fi |
2042 | + |
2043 | +# Install / upgrade pip |
2044 | +if [ $install_pip -eq 1 ]; then |
2045 | + pip install --upgrade https://github.com/checkbox/external-tarballs/raw/master/pypi/pip-1.3.1.tar.gz |
2046 | +fi |
2047 | + |
2048 | +# Install coverage if required |
2049 | +if [ $install_coverage -eq 1 ]; then |
2050 | + pip install --upgrade https://github.com/checkbox/external-tarballs/raw/master/pypi/coverage-3.6.tar.gz |
2051 | +fi |
2052 | + |
2053 | +# Install requests if required |
2054 | +if [ $install_requests -eq 1 ]; then |
2055 | + pip install --upgrade https://github.com/checkbox/external-tarballs/raw/master/pypi/requests-1.1.0.tar.gz |
2056 | +fi |
2057 | + |
2058 | +# "develop" plainbox |
2059 | +http_proxy=http://127.0.0.1:9/ python3 setup.py develop |
2060 | + |
2061 | +echo "To activate your virtualenv run:" |
2062 | +echo "$ . $VENV_PATH/bin/activate" |
2063 | |
2064 | === added directory 'plainbox/plainbox' |
2065 | === added directory 'plainbox/plainbox/impl' |
2066 | === added file 'plainbox/plainbox/impl/box.py.OTHER' |
2067 | --- plainbox/plainbox/impl/box.py.OTHER 1970-01-01 00:00:00 +0000 |
2068 | +++ plainbox/plainbox/impl/box.py.OTHER 2013-08-28 12:37:27 +0000 |
2069 | @@ -0,0 +1,130 @@ |
2070 | +# This file is part of Checkbox. |
2071 | +# |
2072 | +# Copyright 2012 Canonical Ltd. |
2073 | +# Written by: |
2074 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2075 | +# |
2076 | +# Checkbox is free software: you can redistribute it and/or modify |
2077 | +# it under the terms of the GNU General Public License as published by |
2078 | +# the Free Software Foundation, either version 3 of the License, or |
2079 | +# (at your option) any later version. |
2080 | +# |
2081 | +# Checkbox is distributed in the hope that it will be useful, |
2082 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2083 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2084 | +# GNU General Public License for more details. |
2085 | +# |
2086 | +# You should have received a copy of the GNU General Public License |
2087 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2088 | + |
2089 | +""" |
2090 | +:mod:`plainbox.impl.box` -- command line interface |
2091 | +================================================== |
2092 | + |
2093 | +.. warning:: |
2094 | + |
2095 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
2096 | +""" |
2097 | + |
2098 | +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter |
2099 | +from argparse import _ as argparse_gettext |
2100 | +from logging import basicConfig |
2101 | +from logging import getLogger |
2102 | +import argparse |
2103 | +import sys |
2104 | + |
2105 | +from plainbox import __version__ as version |
2106 | +from plainbox.impl.applogic import PlainBoxConfig |
2107 | +from plainbox.impl.checkbox import CheckBox |
2108 | +from plainbox.impl.commands.run import RunCommand |
2109 | +from plainbox.impl.commands.selftest import SelfTestCommand |
2110 | +from plainbox.impl.commands.special import SpecialCommand |
2111 | +from plainbox.impl.commands.sru import SRUCommand |
2112 | +from plainbox.impl.commands.check_config import CheckConfigCommand |
2113 | + |
2114 | + |
2115 | +logger = getLogger("plainbox.box") |
2116 | + |
2117 | + |
2118 | +class PlainBox: |
2119 | + """ |
2120 | + High-level plainbox object |
2121 | + """ |
2122 | + |
2123 | + def __init__(self): |
2124 | + self._checkbox = CheckBox() |
2125 | + |
2126 | + def main(self, argv=None): |
2127 | + # TODO: setup sane logging system that works just as well for Joe user |
2128 | + # that runs checkbox from the CD as well as for checkbox developers and |
2129 | + # custom debugging needs. It would be perfect^Hdesirable not to create |
2130 | + # another broken, never-rotated, uncapped logging crap that kills my |
2131 | + # SSD by writing junk to ~/.cache/ |
2132 | + basicConfig(level="WARNING") |
2133 | + config = PlainBoxConfig.get() |
2134 | + parser = self._construct_parser(config) |
2135 | + ns = parser.parse_args(argv) |
2136 | + # Set the desired log level |
2137 | + getLogger("").setLevel(ns.log_level) |
2138 | + # Argh the horrror! |
2139 | + # |
2140 | + # Since CPython revision cab204a79e09 (landed for python3.3) |
2141 | + # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py |
2142 | + # the argparse module behaves differently than it did in python3.2 |
2143 | + # |
2144 | + # In practical terms subparsers are now optional in 3.3 so all of the |
2145 | + # commands are no longer required parameters. |
2146 | + # |
2147 | + # To compensate, on python3.3 and beyond, when the user just runs |
2148 | + # plainbox without specifying the command, we manually, explicitly do |
2149 | + # what python3.2 did: call parser.error(_('too few arguments')) |
2150 | + if (sys.version_info[:2] >= (3, 3) |
2151 | + and getattr(ns, "command", None) is None): |
2152 | + parser.error(argparse_gettext("too few arguments")) |
2153 | + else: |
2154 | + return ns.command.invoked(ns) |
2155 | + |
2156 | + def _construct_parser(self, config): |
2157 | + parser = ArgumentParser( |
2158 | + prog="plainbox", formatter_class=ArgumentDefaultsHelpFormatter) |
2159 | + parser.add_argument( |
2160 | + "-v", "--version", action="version", |
2161 | + version="{}.{}.{}".format(*version[:3])) |
2162 | + parser.add_argument( |
2163 | + "-l", "--log-level", action="store", |
2164 | + choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), |
2165 | + default='WARNING', |
2166 | + help=argparse.SUPPRESS) |
2167 | + subparsers = parser.add_subparsers() |
2168 | + RunCommand(self._checkbox).register_parser(subparsers) |
2169 | + SpecialCommand(self._checkbox).register_parser(subparsers) |
2170 | + SelfTestCommand().register_parser(subparsers) |
2171 | + SRUCommand(self._checkbox, config).register_parser(subparsers) |
2172 | + CheckConfigCommand(config).register_parser(subparsers) |
2173 | + #group = parser.add_argument_group(title="user interface options") |
2174 | + #group.add_argument( |
2175 | + # "-u", "--ui", action="store", |
2176 | + # default=None, choices=('headless', 'text', 'graphics'), |
2177 | + # help="select the UI front-end (defaults to auto)") |
2178 | + return parser |
2179 | + |
2180 | + |
2181 | +def main(argv=None): |
2182 | + # Instantiate a global plainbox instance |
2183 | + # XXX: Allow one to control the checkbox= argument via |
2184 | + # environment or config. |
2185 | + box = PlainBox() |
2186 | + retval = box.main(argv) |
2187 | + raise SystemExit(retval) |
2188 | + |
2189 | + |
2190 | +def get_builtin_jobs(): |
2191 | + raise NotImplementedError("get_builtin_jobs() not implemented") |
2192 | + |
2193 | + |
2194 | +def save(something, somewhere): |
2195 | + raise NotImplementedError("save() not implemented") |
2196 | + |
2197 | + |
2198 | +def run(*args, **kwargs): |
2199 | + raise NotImplementedError("run() not implemented") |
2200 | |
2201 | === added directory 'plainbox/plainbox/impl/commands' |
2202 | === added file 'plainbox/plainbox/impl/commands/checkbox.py.OTHER' |
2203 | --- plainbox/plainbox/impl/commands/checkbox.py.OTHER 1970-01-01 00:00:00 +0000 |
2204 | +++ plainbox/plainbox/impl/commands/checkbox.py.OTHER 2013-08-28 12:37:27 +0000 |
2205 | @@ -0,0 +1,100 @@ |
2206 | +# This file is part of Checkbox. |
2207 | +# |
2208 | +# Copyright 2012-2013 Canonical Ltd. |
2209 | +# Written by: |
2210 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2211 | +# |
2212 | +# Checkbox is free software: you can redistribute it and/or modify |
2213 | +# it under the terms of the GNU General Public License as published by |
2214 | +# the Free Software Foundation, either version 3 of the License, or |
2215 | +# (at your option) any later version. |
2216 | +# |
2217 | +# Checkbox is distributed in the hope that it will be useful, |
2218 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2219 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2220 | +# GNU General Public License for more details. |
2221 | +# |
2222 | +# You should have received a copy of the GNU General Public License |
2223 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2224 | + |
2225 | +""" |
2226 | +:mod:`plainbox.impl.commands.checkbox` -- mix-in for checkbox commands |
2227 | +====================================================================== |
2228 | + |
2229 | +.. warning:: |
2230 | + |
2231 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
2232 | +""" |
2233 | + |
2234 | +import re |
2235 | +from argparse import FileType |
2236 | + |
2237 | + |
2238 | +class CheckBoxInvocationMixIn: |
2239 | + |
2240 | + def __init__(self, checkbox): |
2241 | + self.checkbox = checkbox |
2242 | + |
2243 | + def get_job_list(self, ns): |
2244 | + """ |
2245 | + Load and return a list of JobDefinition instances |
2246 | + """ |
2247 | + return self.checkbox.get_builtin_jobs() |
2248 | + |
2249 | + def _get_matching_job_list(self, ns, job_list): |
2250 | + # Find jobs that matched patterns |
2251 | + matching_job_list = [] |
2252 | + # Pre-seed the include pattern list with data read from |
2253 | + # the whitelist file. |
2254 | + if ns.whitelist: |
2255 | + ns.include_pattern_list.extend([ |
2256 | + pattern.strip() |
2257 | + for pattern in ns.whitelist.readlines()]) |
2258 | + # Decide which of the known jobs to include |
2259 | + for job in job_list: |
2260 | + # Reject all jobs that match any of the exclude |
2261 | + # patterns, matching strictly from the start to |
2262 | + # the end of the line. |
2263 | + for pattern in ns.exclude_pattern_list: |
2264 | + regexp_pattern = r"^{pattern}$".format(pattern=pattern) |
2265 | + if re.match(regexp_pattern, job.name): |
2266 | + break |
2267 | + else: |
2268 | + # Then accept (include) all job that matches |
2269 | + # any of include patterns, matching strictly |
2270 | + # from the start to the end of the line. |
2271 | + for pattern in ns.include_pattern_list: |
2272 | + regexp_pattern = r"^{pattern}$".format(pattern=pattern) |
2273 | + if re.match(regexp_pattern, job.name): |
2274 | + matching_job_list.append(job) |
2275 | + break |
2276 | + return matching_job_list |
2277 | + |
2278 | + |
2279 | +class CheckBoxCommandMixIn: |
2280 | + """ |
2281 | + Mix-in class for plainbox commands that want to discover and load checkbox |
2282 | + jobs |
2283 | + """ |
2284 | + |
2285 | + def enhance_parser(self, parser): |
2286 | + """ |
2287 | + Add common options for job selection to an existing parser |
2288 | + """ |
2289 | + group = parser.add_argument_group(title="job definition options") |
2290 | + group.add_argument( |
2291 | + '-i', '--include-pattern', action="append", |
2292 | + metavar='PATTERN', default=[], dest='include_pattern_list', |
2293 | + help=("Run jobs matching the given regular expression. Matches " |
2294 | + "from the start to the end of the line.")) |
2295 | + group.add_argument( |
2296 | + '-x', '--exclude-pattern', action="append", |
2297 | + metavar="PATTERN", default=[], dest='exclude_pattern_list', |
2298 | + help=("Do not run jobs matching the given regular expression. " |
2299 | + "Matches from the start to the end of the line.")) |
2300 | + # TODO: Find a way to handle the encoding of the file |
2301 | + group.add_argument( |
2302 | + '-w', '--whitelist', |
2303 | + metavar="WHITELIST", |
2304 | + type=FileType("rt"), |
2305 | + help="Load whitelist containing run patterns") |
2306 | |
2307 | === added file 'plainbox/plainbox/impl/commands/run.py.OTHER' |
2308 | --- plainbox/plainbox/impl/commands/run.py.OTHER 1970-01-01 00:00:00 +0000 |
2309 | +++ plainbox/plainbox/impl/commands/run.py.OTHER 2013-08-28 12:37:27 +0000 |
2310 | @@ -0,0 +1,340 @@ |
2311 | +# This file is part of Checkbox. |
2312 | +# |
2313 | +# Copyright 2012-2013 Canonical Ltd. |
2314 | +# Written by: |
2315 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2316 | +# |
2317 | +# Checkbox is free software: you can redistribute it and/or modify |
2318 | +# it under the terms of the GNU General Public License as published by |
2319 | +# the Free Software Foundation, either version 3 of the License, or |
2320 | +# (at your option) any later version. |
2321 | +# |
2322 | +# Checkbox is distributed in the hope that it will be useful, |
2323 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2324 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2325 | +# GNU General Public License for more details. |
2326 | +# |
2327 | +# You should have received a copy of the GNU General Public License |
2328 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2329 | + |
2330 | +""" |
2331 | +:mod:`plainbox.impl.commands.run` -- run sub-command |
2332 | +==================================================== |
2333 | + |
2334 | +.. warning:: |
2335 | + |
2336 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
2337 | +""" |
2338 | + |
2339 | +from argparse import FileType |
2340 | +from logging import getLogger |
2341 | +from os.path import join |
2342 | +from shutil import copyfileobj |
2343 | +import io |
2344 | +import sys |
2345 | + |
2346 | +from requests.exceptions import ConnectionError, InvalidSchema, HTTPError |
2347 | + |
2348 | +from plainbox.impl.commands import PlainBoxCommand |
2349 | +from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn |
2350 | +from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn |
2351 | +from plainbox.impl.depmgr import DependencyDuplicateError |
2352 | +from plainbox.impl.exporter import ByteStringStreamTranslator |
2353 | +from plainbox.impl.exporter import get_all_exporters |
2354 | +from plainbox.impl.result import JobResult |
2355 | +from plainbox.impl.runner import JobRunner |
2356 | +from plainbox.impl.runner import authenticate_warmup |
2357 | +from plainbox.impl.runner import slugify |
2358 | +from plainbox.impl.session import SessionState |
2359 | +from plainbox.impl.transport import get_all_transports |
2360 | + |
2361 | + |
2362 | +logger = getLogger("plainbox.commands.run") |
2363 | + |
2364 | + |
2365 | +class RunInvocation(CheckBoxInvocationMixIn): |
2366 | + |
2367 | + def __init__(self, checkbox, ns): |
2368 | + super(RunInvocation, self).__init__(checkbox) |
2369 | + self.ns = ns |
2370 | + |
2371 | + def run(self): |
2372 | + ns = self.ns |
2373 | + if ns.output_format == '?': |
2374 | + self._print_output_format_list(ns) |
2375 | + return 0 |
2376 | + elif ns.output_options == '?': |
2377 | + self._print_output_option_list(ns) |
2378 | + return 0 |
2379 | + elif ns.transport == '?': |
2380 | + self._print_transport_list(ns) |
2381 | + return 0 |
2382 | + else: |
2383 | + exporter = self._prepare_exporter(ns) |
2384 | + transport = self._prepare_transport(ns) |
2385 | + job_list = self.get_job_list(ns) |
2386 | + return self._run_jobs(ns, job_list, exporter, transport) |
2387 | + |
2388 | + def _print_output_format_list(self, ns): |
2389 | + print("Available output formats: {}".format( |
2390 | + ', '.join(get_all_exporters()))) |
2391 | + |
2392 | + def _print_output_option_list(self, ns): |
2393 | + print("Each format may support a different set of options") |
2394 | + for name, exporter_cls in get_all_exporters().items(): |
2395 | + print("{}: {}".format( |
2396 | + name, ", ".join(exporter_cls.supported_option_list))) |
2397 | + |
2398 | + def _print_transport_list(self, ns): |
2399 | + print("Available transports: {}".format( |
2400 | + ', '.join(get_all_transports()))) |
2401 | + |
2402 | + def _prepare_exporter(self, ns): |
2403 | + exporter_cls = get_all_exporters()[ns.output_format] |
2404 | + if ns.output_options: |
2405 | + option_list = ns.output_options.split(',') |
2406 | + else: |
2407 | + option_list = None |
2408 | + try: |
2409 | + exporter = exporter_cls(option_list) |
2410 | + except ValueError as exc: |
2411 | + raise SystemExit(str(exc)) |
2412 | + return exporter |
2413 | + |
2414 | + def _prepare_transport(self, ns): |
2415 | + if ns.transport not in get_all_transports(): |
2416 | + return None |
2417 | + transport_cls = get_all_transports()[ns.transport] |
2418 | + try: |
2419 | + return transport_cls(ns.transport_where, ns.transport_options) |
2420 | + except ValueError as exc: |
2421 | + raise SystemExit(str(exc)) |
2422 | + |
2423 | + def ask_for_resume(self, prompt=None, allowed=None): |
2424 | + # FIXME: Add support/callbacks for a GUI |
2425 | + if prompt is None: |
2426 | + prompt = "Do you want to resume the previous session [Y/n]? " |
2427 | + if allowed is None: |
2428 | + allowed = ('', 'y', 'Y', 'n', 'N') |
2429 | + answer = None |
2430 | + while answer not in allowed: |
2431 | + answer = input(prompt) |
2432 | + return False if answer in ('n', 'N') else True |
2433 | + |
2434 | + def _run_jobs(self, ns, job_list, exporter, transport=None): |
2435 | + # Ask the password before anything else in order to run jobs requiring |
2436 | + # privileges |
2437 | + print("[ Authentication ]".center(80, '=')) |
2438 | + return_code = authenticate_warmup() |
2439 | + if return_code: |
2440 | + raise SystemExit(return_code) |
2441 | + # Compute the run list, this can give us notification about problems in |
2442 | + # the selected jobs. Currently we just display each problem |
2443 | + matching_job_list = self._get_matching_job_list(ns, job_list) |
2444 | + print("[ Analyzing Jobs ]".center(80, '=')) |
2445 | + # Create a session that handles most of the stuff needed to run jobs |
2446 | + try: |
2447 | + session = SessionState(job_list) |
2448 | + except DependencyDuplicateError as exc: |
2449 | + # Handle possible DependencyDuplicateError that can happen if |
2450 | + # someone is using plainbox for job development. |
2451 | + print("The job database you are currently using is broken") |
2452 | + print("At least two jobs contend for the name {0}".format( |
2453 | + exc.job.name)) |
2454 | + print("First job defined in: {0}".format(exc.job.origin)) |
2455 | + print("Second job defined in: {0}".format( |
2456 | + exc.duplicate_job.origin)) |
2457 | + raise SystemExit(exc) |
2458 | + with session.open(): |
2459 | + if session.previous_session_file(): |
2460 | + if self.ask_for_resume(): |
2461 | + session.resume() |
2462 | + else: |
2463 | + session.clean() |
2464 | + self._update_desired_job_list(session, matching_job_list) |
2465 | + if (sys.stdin.isatty() and sys.stdout.isatty() and not |
2466 | + ns.not_interactive): |
2467 | + outcome_callback = self.ask_for_outcome |
2468 | + else: |
2469 | + outcome_callback = None |
2470 | + runner = JobRunner( |
2471 | + session.session_dir, |
2472 | + session.jobs_io_log_dir, |
2473 | + outcome_callback=outcome_callback, |
2474 | + dry_run=ns.dry_run |
2475 | + ) |
2476 | + self._run_jobs_with_session(ns, session, runner) |
2477 | + # Get a stream with exported session data. |
2478 | + exported_stream = io.BytesIO() |
2479 | + data_subset = exporter.get_session_data_subset(session) |
2480 | + exporter.dump(data_subset, exported_stream) |
2481 | + exported_stream.seek(0) # Need to rewind the file, puagh |
2482 | + # Write the stream to file if requested |
2483 | + self._save_results(ns.output_file, exported_stream) |
2484 | + # Invoke the transport? |
2485 | + if transport: |
2486 | + exported_stream.seek(0) |
2487 | + try: |
2488 | + transport.send(exported_stream.read()) |
2489 | + except InvalidSchema as exc: |
2490 | + print("Invalid destination URL: {0}".format(exc)) |
2491 | + except ConnectionError as exc: |
2492 | + print(("Unable to connect " |
2493 | + "to destination URL: {0}").format(exc)) |
2494 | + except HTTPError as exc: |
2495 | + print(("Server returned an error when " |
2496 | + "receiving or processing: {0}").format(exc)) |
2497 | + |
2498 | + # FIXME: sensible return value |
2499 | + return 0 |
2500 | + |
2501 | + def _save_results(self, output_file, input_stream): |
2502 | + if output_file is sys.stdout: |
2503 | + print("[ Results ]".center(80, '=')) |
2504 | + # This requires a bit more finesse, as exporters output bytes |
2505 | + # and stdout needs a string. |
2506 | + translating_stream = ByteStringStreamTranslator( |
2507 | + output_file, "utf-8") |
2508 | + copyfileobj(input_stream, translating_stream) |
2509 | + else: |
2510 | + print("Saving results to {}".format(output_file.name)) |
2511 | + copyfileobj(input_stream, output_file) |
2512 | + if output_file is not sys.stdout: |
2513 | + output_file.close() |
2514 | + |
2515 | + def ask_for_outcome(self, prompt=None, allowed=None): |
2516 | + if prompt is None: |
2517 | + prompt = "what is the outcome? " |
2518 | + if allowed is None: |
2519 | + allowed = (JobResult.OUTCOME_PASS, |
2520 | + JobResult.OUTCOME_FAIL, |
2521 | + JobResult.OUTCOME_SKIP) |
2522 | + answer = None |
2523 | + while answer not in allowed: |
2524 | + print("Allowed answers are: {}".format(", ".join(allowed))) |
2525 | + answer = input(prompt) |
2526 | + return answer |
2527 | + |
2528 | + def _update_desired_job_list(self, session, desired_job_list): |
2529 | + problem_list = session.update_desired_job_list(desired_job_list) |
2530 | + if problem_list: |
2531 | + print("[ Warning ]".center(80, '*')) |
2532 | + print("There were some problems with the selected jobs") |
2533 | + for problem in problem_list: |
2534 | + print(" * {}".format(problem)) |
2535 | + print("Problematic jobs will not be considered") |
2536 | + |
2537 | + def _run_jobs_with_session(self, ns, session, runner): |
2538 | + # TODO: run all resource jobs concurrently with multiprocessing |
2539 | + # TODO: make local job discovery nicer, it would be best if |
2540 | + # desired_jobs could be managed entirely internally by SesionState. In |
2541 | + # such case the list of jobs to run would be changed during iteration |
2542 | + # but would be otherwise okay). |
2543 | + print("[ Running All Jobs ]".center(80, '=')) |
2544 | + again = True |
2545 | + while again: |
2546 | + again = False |
2547 | + for job in session.run_list: |
2548 | + # Skip jobs that already have result, this is only needed when |
2549 | + # we run over the list of jobs again, after discovering new |
2550 | + # jobs via the local job output |
2551 | + if session.job_state_map[job.name].result.outcome is not None: |
2552 | + continue |
2553 | + self._run_single_job_with_session(ns, session, runner, job) |
2554 | + session.persistent_save() |
2555 | + if job.plugin == "local": |
2556 | + # After each local job runs rebuild the list of matching |
2557 | + # jobs and run everything again |
2558 | + new_matching_job_list = self._get_matching_job_list( |
2559 | + ns, session.job_list) |
2560 | + self._update_desired_job_list( |
2561 | + session, new_matching_job_list) |
2562 | + again = True |
2563 | + break |
2564 | + |
2565 | + def _run_single_job_with_session(self, ns, session, runner, job): |
2566 | + print("[ {} ]".format(job.name).center(80, '-')) |
2567 | + if job.description is not None: |
2568 | + print(job.description) |
2569 | + print("^" * len(job.description.splitlines()[-1])) |
2570 | + print() |
2571 | + job_state = session.job_state_map[job.name] |
2572 | + logger.debug("Job name: %s", job.name) |
2573 | + logger.debug("Plugin: %s", job.plugin) |
2574 | + logger.debug("Direct dependencies: %s", job.get_direct_dependencies()) |
2575 | + logger.debug("Resource dependencies: %s", |
2576 | + job.get_resource_dependencies()) |
2577 | + logger.debug("Resource program: %r", job.requires) |
2578 | + logger.debug("Command: %r", job.command) |
2579 | + logger.debug("Can start: %s", job_state.can_start()) |
2580 | + logger.debug("Readiness: %s", job_state.get_readiness_description()) |
2581 | + if job_state.can_start(): |
2582 | + print("Running... (output in {}.*)".format( |
2583 | + join(session.jobs_io_log_dir, slugify(job.name)))) |
2584 | + job_result = runner.run_job(job) |
2585 | + print("Outcome: {}".format(job_result.outcome)) |
2586 | + print("Comments: {}".format(job_result.comments)) |
2587 | + else: |
2588 | + job_result = JobResult({ |
2589 | + 'job': job, |
2590 | + 'outcome': JobResult.OUTCOME_NOT_SUPPORTED, |
2591 | + 'comments': job_state.get_readiness_description() |
2592 | + }) |
2593 | + if job_result is not None: |
2594 | + session.update_job_result(job, job_result) |
2595 | + |
2596 | + |
2597 | +class RunCommand(PlainBoxCommand, CheckBoxCommandMixIn): |
2598 | + |
2599 | + def __init__(self, checkbox): |
2600 | + self.checkbox = checkbox |
2601 | + |
2602 | + def invoked(self, ns): |
2603 | + return RunInvocation(self.checkbox, ns).run() |
2604 | + |
2605 | + def register_parser(self, subparsers): |
2606 | + parser = subparsers.add_parser("run", help="run a test job") |
2607 | + parser.set_defaults(command=self) |
2608 | + group = parser.add_argument_group(title="user interface options") |
2609 | + group.add_argument( |
2610 | + '--not-interactive', action='store_true', |
2611 | + help="Skip tests that require interactivity") |
2612 | + group.add_argument( |
2613 | + '-n', '--dry-run', action='store_true', |
2614 | + help="Don't actually run any jobs") |
2615 | + group = parser.add_argument_group("output options") |
2616 | + assert 'text' in get_all_exporters() |
2617 | + group.add_argument( |
2618 | + '-f', '--output-format', default='text', |
2619 | + metavar='FORMAT', choices=['?'] + list( |
2620 | + get_all_exporters().keys()), |
2621 | + help=('Save test results in the specified FORMAT' |
2622 | + ' (pass ? for a list of choices)')) |
2623 | + group.add_argument( |
2624 | + '-p', '--output-options', default='', |
2625 | + metavar='OPTIONS', |
2626 | + help=('Comma-separated list of options for the export mechanism' |
2627 | + ' (pass ? for a list of choices)')) |
2628 | + group.add_argument( |
2629 | + '-o', '--output-file', default='-', |
2630 | + metavar='FILE', type=FileType("wb"), |
2631 | + help=('Save test results to the specified FILE' |
2632 | + ' (or to stdout if FILE is -)')) |
2633 | + group.add_argument( |
2634 | + '-t', '--transport', |
2635 | + metavar='TRANSPORT', choices=['?'] + list( |
2636 | + get_all_transports().keys()), |
2637 | + help=('use TRANSPORT to send results somewhere' |
2638 | + ' (pass ? for a list of choices)')) |
2639 | + group.add_argument( |
2640 | + '--transport-where', |
2641 | + metavar='WHERE', |
2642 | + help=('Where to send data using the selected transport.' |
2643 | + ' This is passed as-is and is transport-dependent.')) |
2644 | + group.add_argument( |
2645 | + '--transport-options', |
2646 | + metavar='OPTIONS', |
2647 | + help=('Comma-separated list of key-value options (k=v) to ' |
2648 | + ' be passed to the transport.')) |
2649 | + # Call enhance_parser from CheckBoxCommandMixIn |
2650 | + self.enhance_parser(parser) |
2651 | |
2652 | === added file 'plainbox/plainbox/impl/commands/special.py.OTHER' |
2653 | --- plainbox/plainbox/impl/commands/special.py.OTHER 1970-01-01 00:00:00 +0000 |
2654 | +++ plainbox/plainbox/impl/commands/special.py.OTHER 2013-08-28 12:37:27 +0000 |
2655 | @@ -0,0 +1,159 @@ |
2656 | +# This file is part of Checkbox. |
2657 | +# |
2658 | +# Copyright 2012-2013 Canonical Ltd. |
2659 | +# Written by: |
2660 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2661 | +# |
2662 | +# Checkbox is free software: you can redistribute it and/or modify |
2663 | +# it under the terms of the GNU General Public License as published by |
2664 | +# the Free Software Foundation, either version 3 of the License, or |
2665 | +# (at your option) any later version. |
2666 | +# |
2667 | +# Checkbox is distributed in the hope that it will be useful, |
2668 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2669 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2670 | +# GNU General Public License for more details. |
2671 | +# |
2672 | +# You should have received a copy of the GNU General Public License |
2673 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2674 | + |
2675 | +""" |
2676 | +:mod:`plainbox.impl.commands.special` -- special sub-command |
2677 | +============================================================ |
2678 | + |
2679 | +.. warning:: |
2680 | + |
2681 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
2682 | +""" |
2683 | + |
2684 | +from logging import getLogger |
2685 | + |
2686 | +from plainbox.impl.commands import PlainBoxCommand |
2687 | +from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn |
2688 | +from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn |
2689 | + |
2690 | + |
2691 | +logger = getLogger("plainbox.commands.special") |
2692 | + |
2693 | + |
2694 | +class SpecialInvocation(CheckBoxInvocationMixIn): |
2695 | + |
2696 | + def __init__(self, checkbox, ns): |
2697 | + super(SpecialInvocation, self).__init__(checkbox) |
2698 | + self.ns = ns |
2699 | + |
2700 | + def run(self): |
2701 | + ns = self.ns |
2702 | + job_list = self.get_job_list(ns) |
2703 | + # Now either do a special action or run the jobs |
2704 | + if ns.special == "list-jobs": |
2705 | + self._print_job_list(ns, job_list) |
2706 | + elif ns.special == "list-job-hashes": |
2707 | + self._print_job_hash_list(ns, job_list) |
2708 | + elif ns.special == "list-expr": |
2709 | + self._print_expression_list(ns, job_list) |
2710 | + elif ns.special == "dep-graph": |
2711 | + self._print_dot_graph(ns, job_list) |
2712 | + # Always succeed |
2713 | + return 0 |
2714 | + |
2715 | + def _get_matching_job_list(self, ns, job_list): |
2716 | + matching_job_list = super( |
2717 | + SpecialInvocation, self)._get_matching_job_list(ns, job_list) |
2718 | + # As a special exception, when ns.special is set and we're either |
2719 | + # listing jobs or job dependencies then when no run pattern was |
2720 | + # specified just operate on the whole set. The ns.special check |
2721 | + # prevents people starting plainbox from accidentally running _all_ |
2722 | + # jobs without prompting. |
2723 | + if ns.special is not None and not ns.include_pattern_list: |
2724 | + matching_job_list = job_list |
2725 | + return matching_job_list |
2726 | + |
2727 | + def _print_job_list(self, ns, job_list): |
2728 | + matching_job_list = self._get_matching_job_list(ns, job_list) |
2729 | + for job in matching_job_list: |
2730 | + print("{}".format(job)) |
2731 | + |
2732 | + def _print_job_hash_list(self, ns, job_list): |
2733 | + matching_job_list = self._get_matching_job_list(ns, job_list) |
2734 | + for job in matching_job_list: |
2735 | + print("{} {}".format(job.get_checksum(), job)) |
2736 | + |
2737 | + def _print_expression_list(self, ns, job_list): |
2738 | + matching_job_list = self._get_matching_job_list(ns, job_list) |
2739 | + expressions = set() |
2740 | + for job in matching_job_list: |
2741 | + prog = job.get_resource_program() |
2742 | + if prog is not None: |
2743 | + for expression in prog.expression_list: |
2744 | + expressions.add(expression.text) |
2745 | + for expression in sorted(expressions): |
2746 | + print(expression) |
2747 | + |
2748 | + def _print_dot_graph(self, ns, job_list): |
2749 | + matching_job_list = self._get_matching_job_list(ns, job_list) |
2750 | + print('digraph dependency_graph {') |
2751 | + print('\tnode [shape=box];') |
2752 | + for job in matching_job_list: |
2753 | + if job.plugin == "resource": |
2754 | + print('\t"{}" [shape=ellipse,color=blue];'.format(job.name)) |
2755 | + elif job.plugin == "attachment": |
2756 | + print('\t"{}" [color=green];'.format(job.name)) |
2757 | + elif job.plugin == "local": |
2758 | + print('\t"{}" [shape=invtriangle,color=red];'.format( |
2759 | + job.name)) |
2760 | + elif job.plugin == "shell": |
2761 | + print('\t"{}" [];'.format(job.name)) |
2762 | + elif job.plugin in ("manual", "user-verify", "user-interact"): |
2763 | + print('\t"{}" [color=orange];'.format(job.name)) |
2764 | + for dep_name in job.get_direct_dependencies(): |
2765 | + print('\t"{}" -> "{}";'.format(job.name, dep_name)) |
2766 | + prog = job.get_resource_program() |
2767 | + if ns.dot_resources and prog is not None: |
2768 | + for expression in prog.expression_list: |
2769 | + print('\t"{}" [shape=ellipse,color=blue];'.format( |
2770 | + expression.resource_name)) |
2771 | + print('\t"{}" -> "{}" [style=dashed, label="{}"];'.format( |
2772 | + job.name, expression.resource_name, |
2773 | + expression.text.replace('"', "'"))) |
2774 | + print("}") |
2775 | + |
2776 | + |
2777 | +class SpecialCommand(PlainBoxCommand, CheckBoxCommandMixIn): |
2778 | + """ |
2779 | + Implementation of ``$ plainbox special`` |
2780 | + """ |
2781 | + |
2782 | + def __init__(self, checkbox): |
2783 | + self.checkbox = checkbox |
2784 | + |
2785 | + def invoked(self, ns): |
2786 | + return SpecialInvocation(self.checkbox, ns).run() |
2787 | + |
2788 | + def register_parser(self, subparsers): |
2789 | + parser = subparsers.add_parser( |
2790 | + "special", help="special/internal commands") |
2791 | + parser.set_defaults(command=self) |
2792 | + group = parser.add_mutually_exclusive_group(required=True) |
2793 | + group.add_argument( |
2794 | + '-j', '--list-jobs', |
2795 | + help="List jobs instead of running them", |
2796 | + action="store_const", const="list-jobs", dest="special") |
2797 | + group.add_argument( |
2798 | + '-J', '--list-job-hashes', |
2799 | + help="List jobs with hashes instead of running them", |
2800 | + action="store_const", const="list-job-hashes", dest="special") |
2801 | + group.add_argument( |
2802 | + '-e', '--list-expressions', |
2803 | + help="List all unique resource expressions", |
2804 | + action="store_const", const="list-expr", dest="special") |
2805 | + group.add_argument( |
2806 | + '-d', '--dot', |
2807 | + help="Print a graph of jobs instead of running them", |
2808 | + action="store_const", const="dep-graph", dest="special") |
2809 | + parser.add_argument( |
2810 | + '--dot-resources', |
2811 | + help="Render resource relationships (for --dot)", |
2812 | + action='store_true') |
2813 | + # Call enhance_parser from CheckBoxCommandMixIn |
2814 | + self.enhance_parser(parser) |
2815 | |
2816 | === added file 'plainbox/plainbox/impl/commands/sru.py.OTHER' |
2817 | --- plainbox/plainbox/impl/commands/sru.py.OTHER 1970-01-01 00:00:00 +0000 |
2818 | +++ plainbox/plainbox/impl/commands/sru.py.OTHER 2013-08-28 12:37:27 +0000 |
2819 | @@ -0,0 +1,271 @@ |
2820 | +# This file is part of Checkbox. |
2821 | +# |
2822 | +# |
2823 | +# Copyright 2013 Canonical Ltd. |
2824 | +# Written by: |
2825 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2826 | +# |
2827 | +# Checkbox is free software: you can redistribute it and/or modify |
2828 | +# it under the terms of the GNU General Public License as published by |
2829 | +# the Free Software Foundation, either version 3 of the License, or |
2830 | +# (at your option) any later version. |
2831 | +# |
2832 | +# Checkbox is distributed in the hope that it will be useful, |
2833 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2834 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2835 | +# GNU General Public License for more details. |
2836 | +# |
2837 | +# You should have received a copy of the GNU General Public License |
2838 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2839 | + |
2840 | +""" |
2841 | +:mod:`plainbox.impl.commands.sru` -- sru sub-command |
2842 | +==================================================== |
2843 | + |
2844 | +.. warning:: |
2845 | + |
2846 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
2847 | +""" |
2848 | +import logging |
2849 | +import os |
2850 | +import tempfile |
2851 | + |
2852 | +from requests.exceptions import ConnectionError, InvalidSchema, HTTPError |
2853 | + |
2854 | +from plainbox.impl.applogic import get_matching_job_list |
2855 | +from plainbox.impl.applogic import run_job_if_possible |
2856 | +from plainbox.impl.checkbox import WhiteList |
2857 | +from plainbox.impl.commands import PlainBoxCommand |
2858 | +from plainbox.impl.commands.check_config import CheckConfigInvocation |
2859 | +from plainbox.impl.config import ValidationError, Unset |
2860 | +from plainbox.impl.depmgr import DependencyDuplicateError |
2861 | +from plainbox.impl.exporter import ByteStringStreamTranslator |
2862 | +from plainbox.impl.exporter.xml import XMLSessionStateExporter |
2863 | +from plainbox.impl.runner import JobRunner |
2864 | +from plainbox.impl.session import SessionState |
2865 | +from plainbox.impl.transport.certification import CertificationTransport |
2866 | +from plainbox.impl.transport.certification import InvalidSecureIDError |
2867 | + |
2868 | + |
2869 | +logger = logging.getLogger("plainbox.commands.sru") |
2870 | + |
2871 | + |
2872 | +class _SRUInvocation: |
2873 | + """ |
2874 | + Helper class instantiated to perform a particular invocation of the sru |
2875 | + command. Unlike the SRU command itself, this class is instantiated each |
2876 | + time. |
2877 | + """ |
2878 | + |
2879 | + def __init__(self, checkbox, config, ns): |
2880 | + self.checkbox = checkbox |
2881 | + self.config = config |
2882 | + self.ns = ns |
2883 | + self.whitelist = WhiteList.from_file(os.path.join( |
2884 | + self.checkbox.whitelists_dir, "sru.whitelist")) |
2885 | + self.job_list = self.checkbox.get_builtin_jobs() |
2886 | + # XXX: maybe allow specifying system_id from command line? |
2887 | + self.exporter = XMLSessionStateExporter(system_id=None) |
2888 | + self.session = None |
2889 | + self.runner = None |
2890 | + |
2891 | + def run(self): |
2892 | + # Compute the run list, this can give us notification about problems in |
2893 | + # the selected jobs. Currently we just display each problem |
2894 | + # Create a session that handles most of the stuff needed to run jobs |
2895 | + try: |
2896 | + self.session = SessionState(self.job_list) |
2897 | + except DependencyDuplicateError as exc: |
2898 | + # Handle possible DependencyDuplicateError that can happen if |
2899 | + # someone is using plainbox for job development. |
2900 | + print("The job database you are currently using is broken") |
2901 | + print("At least two jobs contend for the name {0}".format( |
2902 | + exc.job.name)) |
2903 | + print("First job defined in: {0}".format(exc.job.origin)) |
2904 | + print("Second job defined in: {0}".format( |
2905 | + exc.duplicate_job.origin)) |
2906 | + raise SystemExit(exc) |
2907 | + with self.session.open(): |
2908 | + self._set_job_selection() |
2909 | + self.runner = JobRunner( |
2910 | + self.session.session_dir, |
2911 | + self.session.jobs_io_log_dir, |
2912 | + command_io_delegate=self, |
2913 | + outcome_callback=None, # SRU runs are never interactive |
2914 | + dry_run=self.ns.dry_run |
2915 | + ) |
2916 | + self._run_all_jobs() |
2917 | + if self.config.fallback_file is not Unset: |
2918 | + self._save_results() |
2919 | + self._submit_results() |
2920 | + # FIXME: sensible return value |
2921 | + return 0 |
2922 | + |
2923 | + def _set_job_selection(self): |
2924 | + desired_job_list = get_matching_job_list(self.job_list, self.whitelist) |
2925 | + problem_list = self.session.update_desired_job_list(desired_job_list) |
2926 | + if problem_list: |
2927 | + logger.warning("There were some problems with the selected jobs") |
2928 | + for problem in problem_list: |
2929 | + logger.warning("- %s", problem) |
2930 | + logger.warning("Problematic jobs will not be considered") |
2931 | + |
2932 | + def _save_results(self): |
2933 | + print("Saving results to {0}".format(self.config.fallback_file)) |
2934 | + data = self.exporter.get_session_data_subset(self.session) |
2935 | + with open(self.config.fallback_file, "wt", encoding="UTF-8") as stream: |
2936 | + translating_stream = ByteStringStreamTranslator(stream, "UTF-8") |
2937 | + self.exporter.dump(data, translating_stream) |
2938 | + |
2939 | + def _submit_results(self): |
2940 | + print("Submitting results to {0} for secure_id {1}".format( |
2941 | + self.config.c3_url, self.config.secure_id)) |
2942 | + options_string = "secure_id={0}".format(self.config.secure_id) |
2943 | + # Create the transport object |
2944 | + try: |
2945 | + transport = CertificationTransport( |
2946 | + self.config.c3_url, options_string, self.config) |
2947 | + except InvalidSecureIDError as exc: |
2948 | + print(exc) |
2949 | + return False |
2950 | + # Prepare the data for submission |
2951 | + data = self.exporter.get_session_data_subset(self.session) |
2952 | + with tempfile.NamedTemporaryFile(mode='w+b') as stream: |
2953 | + # Dump the data to the temporary file |
2954 | + self.exporter.dump(data, stream) |
2955 | + # Flush and rewind |
2956 | + stream.flush() |
2957 | + stream.seek(0) |
2958 | + try: |
2959 | + # Send the data, reading from the temporary file |
2960 | + result = transport.send(stream) |
2961 | + if 'url' in result: |
2962 | + print("Successfully sent, submission status at {0}".format( |
2963 | + result['url'])) |
2964 | + else: |
2965 | + print("Successfully sent, server response: {0}".format( |
2966 | + result)) |
2967 | + |
2968 | + except InvalidSchema as exc: |
2969 | + print("Invalid destination URL: {0}".format(exc)) |
2970 | + except ConnectionError as exc: |
2971 | + print("Unable to connect to destination URL: {0}".format(exc)) |
2972 | + except HTTPError as exc: |
2973 | + print(("Server returned an error when " |
2974 | + "receiving or processing: {0}").format(exc)) |
2975 | + except IOError as exc: |
2976 | + print("Problem reading a file: {0}".format(exc)) |
2977 | + |
2978 | + def _run_all_jobs(self): |
2979 | + again = True |
2980 | + while again: |
2981 | + again = False |
2982 | + for job in self.session.run_list: |
2983 | + # Skip jobs that already have result, this is only needed when |
2984 | + # we run over the list of jobs again, after discovering new |
2985 | + # jobs via the local job output |
2986 | + result = self.session.job_state_map[job.name].result |
2987 | + if result.outcome is not None: |
2988 | + continue |
2989 | + self._run_single_job(job) |
2990 | + self.session.persistent_save() |
2991 | + if job.plugin == "local": |
2992 | + # After each local job runs rebuild the list of matching |
2993 | + # jobs and run everything again |
2994 | + self._set_job_selection() |
2995 | + again = True |
2996 | + break |
2997 | + |
2998 | + def _run_single_job(self, job): |
2999 | + print("- {}:".format(job.name), end=' ') |
3000 | + job_state, job_result = run_job_if_possible( |
3001 | + self.session, self.runner, self.config, job) |
3002 | + print("{0}".format(job_result.outcome)) |
3003 | + if job_result.comments is not None: |
3004 | + print("comments: {0}".format(job_result.comments)) |
3005 | + if job_state.readiness_inhibitor_list: |
3006 | + print("inhibitors:") |
3007 | + for inhibitor in job_state.readiness_inhibitor_list: |
3008 | + print(" * {}".format(inhibitor)) |
3009 | + self.session.update_job_result(job, job_result) |
3010 | + |
3011 | + |
3012 | +class SRUCommand(PlainBoxCommand): |
3013 | + """ |
3014 | + Command for running Stable Release Update (SRU) tests. |
3015 | + |
3016 | + Stable release updates are periodic fixes for nominated bugs that land in |
3017 | + existing supported Ubuntu releases. To ensure a certain level of quality |
3018 | + all SRU updates affecting hardware enablement are automatically tested |
3019 | + on a pool of certified machines. |
3020 | + |
3021 | + This command is _temporary_ and will eventually migrate to the checkbox |
3022 | + side. Its intended lifecycle is for the development and validation of |
3023 | + plainbox core on realistic workloads. |
3024 | + """ |
3025 | + |
3026 | + def __init__(self, checkbox, config): |
3027 | + self.checkbox = checkbox |
3028 | + self.config = config |
3029 | + |
3030 | + def invoked(self, ns): |
3031 | + # Copy command-line arguments over configuration variables |
3032 | + try: |
3033 | + if ns.secure_id: |
3034 | + self.config.secure_id = ns.secure_id |
3035 | + if ns.fallback_file and ns.fallback_file is not Unset: |
3036 | + self.config.fallback_file = ns.fallback_file |
3037 | + if ns.c3_url: |
3038 | + self.config.c3_url = ns.c3_url |
3039 | + except ValidationError as exc: |
3040 | + print("Configuration problems prevent running SRU tests") |
3041 | + print(exc) |
3042 | + return 1 |
3043 | + # Run check-config, if requested |
3044 | + if ns.check_config: |
3045 | + retval = CheckConfigInvocation(self.config).run() |
3046 | + if retval != 0: |
3047 | + return retval |
3048 | + return _SRUInvocation(self.checkbox, self.config, ns).run() |
3049 | + |
3050 | + def register_parser(self, subparsers): |
3051 | + parser = subparsers.add_parser( |
3052 | + "sru", help="run automated stable release update tests") |
3053 | + parser.set_defaults(command=self) |
3054 | + parser.add_argument( |
3055 | + "--check-config", |
3056 | + action="store_true", |
3057 | + help="Run plainbox check-config before starting") |
3058 | + group = parser.add_argument_group("sru-specific options") |
3059 | + # Set defaults from based on values from the config file |
3060 | + group.set_defaults( |
3061 | + secure_id=self.config.secure_id, |
3062 | + c3_url=self.config.c3_url, |
3063 | + fallback_file=self.config.fallback_file) |
3064 | + group.add_argument( |
3065 | + '--secure-id', metavar="SECURE-ID", |
3066 | + action='store', |
3067 | + # NOTE: --secure-id is optional only when set in a config file |
3068 | + required=self.config.secure_id is Unset, |
3069 | + help=("Associate submission with a machine using this SECURE-ID" |
3070 | + " (%(default)s)")) |
3071 | + group.add_argument( |
3072 | + '--fallback', metavar="FILE", |
3073 | + dest='fallback_file', |
3074 | + action='store', |
3075 | + default=Unset, |
3076 | + help=("If submission fails save the test report as FILE" |
3077 | + " (%(default)s)")) |
3078 | + group.add_argument( |
3079 | + '--destination', metavar="URL", |
3080 | + dest='c3_url', |
3081 | + action='store', |
3082 | + help=("POST the test report XML to this URL" |
3083 | + " (%(default)s)")) |
3084 | + group = parser.add_argument_group(title="execution options") |
3085 | + group.add_argument( |
3086 | + '-n', '--dry-run', |
3087 | + action='store_true', |
3088 | + default=False, |
3089 | + help=("Skip all usual jobs." |
3090 | + " Only local, resource and attachment jobs are started")) |
3091 | |
3092 | === added file 'plainbox/plainbox/impl/commands/test_run.py.OTHER' |
3093 | --- plainbox/plainbox/impl/commands/test_run.py.OTHER 1970-01-01 00:00:00 +0000 |
3094 | +++ plainbox/plainbox/impl/commands/test_run.py.OTHER 2013-08-28 12:37:27 +0000 |
3095 | @@ -0,0 +1,142 @@ |
3096 | +# This file is part of Checkbox. |
3097 | +# |
3098 | +# Copyright 2013 Canonical Ltd. |
3099 | +# Written by: |
3100 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
3101 | +# Daniel Manrique <roadmr@ubuntu.com> |
3102 | +# |
3103 | +# Checkbox is free software: you can redistribute it and/or modify |
3104 | +# it under the terms of the GNU General Public License as published by |
3105 | +# the Free Software Foundation, either version 3 of the License, or |
3106 | +# (at your option) any later version. |
3107 | +# |
3108 | +# Checkbox is distributed in the hope that it will be useful, |
3109 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3110 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3111 | +# GNU General Public License for more details. |
3112 | +# |
3113 | +# You should have received a copy of the GNU General Public License |
3114 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
3115 | + |
3116 | +""" |
3117 | +plainbox.impl.commands.test_run |
3118 | +=============================== |
3119 | + |
3120 | +Test definitions for plainbox.impl.run module |
3121 | +""" |
3122 | + |
3123 | +import os |
3124 | +import shutil |
3125 | +import tempfile |
3126 | + |
3127 | +from inspect import cleandoc |
3128 | +from mock import patch |
3129 | +from unittest import TestCase |
3130 | + |
3131 | +from plainbox.impl.box import main |
3132 | +from plainbox.testing_utils.io import TestIO |
3133 | + |
3134 | + |
3135 | +class TestRun(TestCase): |
3136 | + |
3137 | + def setUp(self): |
3138 | + # session data are kept in XDG_CACHE_HOME/plainbox/.session |
3139 | + # To avoid resuming a real session, we have to select a temporary |
3140 | + # location instead |
3141 | + self._sandbox = tempfile.mkdtemp() |
3142 | + self._env = os.environ |
3143 | + os.environ['XDG_CACHE_HOME'] = self._sandbox |
3144 | + |
3145 | + def test_help(self): |
3146 | + with TestIO(combined=True) as io: |
3147 | + with self.assertRaises(SystemExit) as call: |
3148 | + main(['run', '--help']) |
3149 | + self.assertEqual(call.exception.args, (0,)) |
3150 | + self.maxDiff = None |
3151 | + expected = """ |
3152 | + usage: plainbox run [-h] [--not-interactive] [-n] [-f FORMAT] [-p OPTIONS] |
3153 | + [-o FILE] [-t TRANSPORT] [--transport-where WHERE] |
3154 | + [--transport-options OPTIONS] [-i PATTERN] [-x PATTERN] |
3155 | + [-w WHITELIST] |
3156 | + |
3157 | + optional arguments: |
3158 | + -h, --help show this help message and exit |
3159 | + |
3160 | + user interface options: |
3161 | + --not-interactive Skip tests that require interactivity |
3162 | + -n, --dry-run Don't actually run any jobs |
3163 | + |
3164 | + output options: |
3165 | + -f FORMAT, --output-format FORMAT |
3166 | + Save test results in the specified FORMAT (pass ? for |
3167 | + a list of choices) |
3168 | + -p OPTIONS, --output-options OPTIONS |
3169 | + Comma-separated list of options for the export |
3170 | + mechanism (pass ? for a list of choices) |
3171 | + -o FILE, --output-file FILE |
3172 | + Save test results to the specified FILE (or to stdout |
3173 | + if FILE is -) |
3174 | + -t TRANSPORT, --transport TRANSPORT |
3175 | + use TRANSPORT to send results somewhere (pass ? for a |
3176 | + list of choices) |
3177 | + --transport-where WHERE |
3178 | + Where to send data using the selected transport. This |
3179 | + is passed as-is and is transport-dependent. |
3180 | + --transport-options OPTIONS |
3181 | + Comma-separated list of key-value options (k=v) to be |
3182 | + passed to the transport. |
3183 | + |
3184 | + job definition options: |
3185 | + -i PATTERN, --include-pattern PATTERN |
3186 | + Run jobs matching the given regular expression. |
3187 | + Matches from the start to the end of the line. |
3188 | + -x PATTERN, --exclude-pattern PATTERN |
3189 | + Do not run jobs matching the given regular expression. |
3190 | + Matches from the start to the end of the line. |
3191 | + -w WHITELIST, --whitelist WHITELIST |
3192 | + Load whitelist containing run patterns |
3193 | + """ |
3194 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
3195 | + |
3196 | + def test_run_without_args(self): |
3197 | + with TestIO(combined=True) as io: |
3198 | + with self.assertRaises(SystemExit) as call: |
3199 | + with patch('plainbox.impl.commands.run.authenticate_warmup') as mock_warmup: |
3200 | + mock_warmup.return_value = 0 |
3201 | + main(['run']) |
3202 | + self.assertEqual(call.exception.args, (0,)) |
3203 | + expected = """ |
3204 | + ===============================[ Authentication ]=============================== |
3205 | + ===============================[ Analyzing Jobs ]=============================== |
3206 | + ==============================[ Running All Jobs ]============================== |
3207 | + ==================================[ Results ]=================================== |
3208 | + """ |
3209 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
3210 | + |
3211 | + def test_output_format_list(self): |
3212 | + with TestIO(combined=True) as io: |
3213 | + with self.assertRaises(SystemExit) as call: |
3214 | + main(['run', '--output-format=?']) |
3215 | + self.assertEqual(call.exception.args, (0,)) |
3216 | + expected = """ |
3217 | + Available output formats: json, rfc822, text, xml |
3218 | + """ |
3219 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
3220 | + |
3221 | + def test_output_option_list(self): |
3222 | + with TestIO(combined=True) as io: |
3223 | + with self.assertRaises(SystemExit) as call: |
3224 | + main(['run', '--output-option=?']) |
3225 | + self.assertEqual(call.exception.args, (0,)) |
3226 | + expected = """ |
3227 | + Each format may support a different set of options |
3228 | + json: with-io-log, squash-io-log, flatten-io-log, with-run-list, with-job-list, with-resource-map, with-job-defs, with-attachments, with-comments, machine-json |
3229 | + rfc822: with-io-log, squash-io-log, flatten-io-log, with-run-list, with-job-list, with-resource-map, with-job-defs, with-attachments, with-comments |
3230 | + text: with-io-log, squash-io-log, flatten-io-log, with-run-list, with-job-list, with-resource-map, with-job-defs, with-attachments, with-comments |
3231 | + xml: |
3232 | + """ |
3233 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
3234 | + |
3235 | + def tearDown(self): |
3236 | + shutil.rmtree(self._sandbox) |
3237 | + os.environ = self._env |
3238 | |
3239 | === added file 'plainbox/plainbox/impl/config.py.OTHER' |
3240 | --- plainbox/plainbox/impl/config.py.OTHER 1970-01-01 00:00:00 +0000 |
3241 | +++ plainbox/plainbox/impl/config.py.OTHER 2013-08-28 12:37:27 +0000 |
3242 | @@ -0,0 +1,544 @@ |
3243 | +# This file is part of Checkbox. |
3244 | +# |
3245 | +# Copyright 2013 Canonical Ltd. |
3246 | +# Written by: |
3247 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
3248 | +# |
3249 | +# Checkbox is free software: you can redistribute it and/or modify |
3250 | +# it under the terms of the GNU General Public License as published by |
3251 | +# the Free Software Foundation, either version 3 of the License, or |
3252 | +# (at your option) any later version. |
3253 | +# |
3254 | +# Checkbox is distributed in the hope that it will be useful, |
3255 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3256 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3257 | +# GNU General Public License for more details. |
3258 | +# |
3259 | +# You should have received a copy of the GNU General Public License |
3260 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
3261 | + |
3262 | +""" |
3263 | +:mod:`plainbox.impl.config` -- configuration |
3264 | +============================================ |
3265 | + |
3266 | +.. warning:: |
3267 | + |
3268 | + THIS MODULE DOES NOT HAVE A STABLE PUBLIC API |
3269 | +""" |
3270 | + |
3271 | +from abc import ABCMeta, abstractmethod |
3272 | +import collections |
3273 | +import configparser |
3274 | +import logging |
3275 | +import re |
3276 | + |
3277 | + |
3278 | +logger = logging.getLogger("plainbox.config") |
3279 | + |
3280 | + |
3281 | +class INameTracking(metaclass=ABCMeta): |
3282 | + """ |
3283 | + Interface for classes that are instantiated as a part of definition of |
3284 | + another class. The purpose of this interface is to allow instances to learn |
3285 | + about the name (python identifier) that was assigned to the instance at |
3286 | + class definition time. |
3287 | + |
3288 | + Subclasses must define the _set_tracked_name() method. |
3289 | + """ |
3290 | + |
3291 | + @abstractmethod |
3292 | + def _set_tracked_name(self, name): |
3293 | + """ |
3294 | + Set the that corresponds to the symbol used in class definition. This |
3295 | + can be a no-op if the name was already set by other means |
3296 | + """ |
3297 | + |
3298 | + |
3299 | +class ConfigMetaData: |
3300 | + """ |
3301 | + Class containing meta-data about a Config class |
3302 | + |
3303 | + Sub-classes of this class are automatically added to each Config subclass |
3304 | + as a Meta class-level attribute. |
3305 | + |
3306 | + This class has typically two attributes: |
3307 | + |
3308 | + :cvar variable_list: |
3309 | + A list of all Variable objects defined in the class |
3310 | + |
3311 | + :cvar section_list: |
3312 | + A list of all Section object defined in the class |
3313 | + |
3314 | + :cvar filename_list: |
3315 | + A list of config files (pathnames) to read on call to |
3316 | + :meth:`Config.read` |
3317 | + """ |
3318 | + variable_list = [] |
3319 | + section_list = [] |
3320 | + filename_list = [] |
3321 | + |
3322 | + |
3323 | +class UnsetType: |
3324 | + """ |
3325 | + Class of the Unset object |
3326 | + """ |
3327 | + |
3328 | + def __str__(self): |
3329 | + return "unset" |
3330 | + |
3331 | + def __repr__(self): |
3332 | + return "Unset" |
3333 | + |
3334 | + |
3335 | +Unset = UnsetType() |
3336 | + |
3337 | + |
3338 | +class Variable(INameTracking): |
3339 | + """ |
3340 | + Variable that can be used in a configuration systems |
3341 | + """ |
3342 | + |
3343 | + _KIND_CHOICE = (bool, int, float, str) |
3344 | + |
3345 | + def __init__(self, name=None, *, section='DEFAULT', kind=str, |
3346 | + default=Unset, validator_list=None, help_text=None): |
3347 | + # Ensure kind is correct |
3348 | + if kind not in self._KIND_CHOICE: |
3349 | + raise ValueError("unsupported kind") |
3350 | + # Ensure that we have a validator_list, even if empty |
3351 | + if validator_list is None: |
3352 | + validator_list = [] |
3353 | + # Insert a KindValidator as the first validator to run |
3354 | + validator_list.insert(0, KindValidator) |
3355 | + # Assign all the attributes |
3356 | + self._name = name |
3357 | + self._section = section |
3358 | + self._kind = kind |
3359 | + self._default = default |
3360 | + self._validator_list = validator_list |
3361 | + self._help_text = help_text |
3362 | + self._validate_default_value() |
3363 | + # Workaround for Sphinx breaking if __doc__ is a property |
3364 | + self.__doc__ = self.help_text or self.__class__.__doc__ |
3365 | + |
3366 | + def _validate_default_value(self): |
3367 | + """ |
3368 | + Validate the default value, unless it is Unset |
3369 | + """ |
3370 | + if self.default is Unset: |
3371 | + return |
3372 | + for validator in self.validator_list: |
3373 | + message = validator(self, self.default) |
3374 | + if message is not None: |
3375 | + raise ValidationError(self, self.default, message) |
3376 | + |
3377 | + def _set_tracked_name(self, name): |
3378 | + """ |
3379 | + Internal method used by :meth:`ConfigMeta.__new__` |
3380 | + """ |
3381 | + if self._name is None: |
3382 | + self._name = name |
3383 | + |
3384 | + @property |
3385 | + def name(self): |
3386 | + """ |
3387 | + name of this variable |
3388 | + """ |
3389 | + return self._name |
3390 | + |
3391 | + @property |
3392 | + def section(self): |
3393 | + """ |
3394 | + name of the section this variable belongs to (in a configuration file) |
3395 | + """ |
3396 | + return self._section |
3397 | + |
3398 | + @property |
3399 | + def kind(self): |
3400 | + """ |
3401 | + the "poor man's type", can be only str (default), bool, float or int |
3402 | + """ |
3403 | + return self._kind |
3404 | + |
3405 | + @property |
3406 | + def default(self): |
3407 | + """ |
3408 | + a default value, if any |
3409 | + """ |
3410 | + return self._default |
3411 | + |
3412 | + @property |
3413 | + def validator_list(self): |
3414 | + """ |
3415 | + a optional list of :class:`Validator` instances that are enforced on |
3416 | + the value |
3417 | + """ |
3418 | + return self._validator_list |
3419 | + |
3420 | + @property |
3421 | + def help_text(self): |
3422 | + """ |
3423 | + an optional help text associated with this variable |
3424 | + """ |
3425 | + return self._help_text |
3426 | + |
3427 | + def __repr__(self): |
3428 | + return "<Variable name:{!r}>".format(self.name) |
3429 | + |
3430 | + def __get__(self, instance, owner): |
3431 | + """ |
3432 | + Get the value of a variable |
3433 | + |
3434 | + Missing variables return the default value |
3435 | + """ |
3436 | + if instance is None: |
3437 | + return self |
3438 | + try: |
3439 | + return instance._get_variable(self._name) |
3440 | + except KeyError: |
3441 | + return self.default |
3442 | + |
3443 | + def __set__(self, instance, new_value): |
3444 | + """ |
3445 | + Set the value of a variable |
3446 | + |
3447 | + :raises ValidationError: if the new value is incorrect |
3448 | + """ |
3449 | + # Check it against all validators |
3450 | + for validator in self.validator_list: |
3451 | + message = validator(self, new_value) |
3452 | + if message is not None: |
3453 | + raise ValidationError(self, new_value, message) |
3454 | + # Assign it to the backing store of the instance |
3455 | + instance._set_variable(self.name, new_value) |
3456 | + |
3457 | + def __delete__(self, instance): |
3458 | + # NOTE: this is quite confusing, this method is a companion to __get__ |
3459 | + # and __set__ but __del__ is entirely unrelated (object garbage |
3460 | + # collected, do final cleanup) so don't think this is a mistake |
3461 | + instance._del_variable(self._name) |
3462 | + |
3463 | + |
3464 | +class Section(INameTracking): |
3465 | + """ |
3466 | + A section of a configuration file. |
3467 | + """ |
3468 | + |
3469 | + def __init__(self, name=None, *, help_text=None): |
3470 | + self._name = name |
3471 | + self._help_text = help_text |
3472 | + # Workaround for Sphinx breaking if __doc__ is a property |
3473 | + self.__doc__ = self.help_text or self.__class__.__doc__ |
3474 | + |
3475 | + def _set_tracked_name(self, name): |
3476 | + """ |
3477 | + Internal method used by :meth:`ConfigMeta.__new__` |
3478 | + """ |
3479 | + if self._name is None: |
3480 | + self._name = name |
3481 | + |
3482 | + @property |
3483 | + def name(self): |
3484 | + """ |
3485 | + name of this section |
3486 | + """ |
3487 | + return self._name |
3488 | + |
3489 | + @property |
3490 | + def help_text(self): |
3491 | + """ |
3492 | + an optional help text associated with this section |
3493 | + """ |
3494 | + return self._help_text |
3495 | + |
3496 | + def __get__(self, instance, owner): |
3497 | + if instance is None: |
3498 | + return self |
3499 | + try: |
3500 | + return instance._get_section(self._name) |
3501 | + except KeyError: |
3502 | + return Unset |
3503 | + |
3504 | + def __set__(self, instance, new_value): |
3505 | + instance._set_section(self.name, new_value) |
3506 | + |
3507 | + def __delete__(self, instance): |
3508 | + instance._del_section(self.name) |
3509 | + |
3510 | + |
3511 | +class ConfigMeta(type): |
3512 | + """ |
3513 | + Meta class for all configuration classes. |
3514 | + |
3515 | + This meta class handles assignment of '_name' attribute to each |
3516 | + :class:`Variable` instance created in the class body. |
3517 | + |
3518 | + It also accumulates such instances and assigns them to variable_list in a |
3519 | + helper Meta class which is assigned back to the namespace |
3520 | + """ |
3521 | + |
3522 | + def __new__(mcls, name, bases, namespace, **kwargs): |
3523 | + # Keep track of variables and sections from base class |
3524 | + variable_list = [] |
3525 | + section_list = [] |
3526 | + if 'Meta' in namespace: |
3527 | + if hasattr(namespace['Meta'], 'variable_list'): |
3528 | + variable_list = namespace['Meta'].variable_list[:] |
3529 | + if hasattr(namespace['Meta'], 'section_list'): |
3530 | + section_list = namespace['Meta'].section_list[:] |
3531 | + # Discover all Variable and Section instances |
3532 | + # defined in the class namespace |
3533 | + for name, item in namespace.items(): |
3534 | + if isinstance(item, INameTracking): |
3535 | + item._set_tracked_name(name) |
3536 | + if isinstance(item, Variable): |
3537 | + variable_list.append(item) |
3538 | + elif isinstance(item, Section): |
3539 | + section_list.append(item) |
3540 | + # Get or create the class of the 'Meta' object. |
3541 | + # |
3542 | + # This class should always inherit from ConfigMetaData and whatever the |
3543 | + # user may have defined as Meta. |
3544 | + Meta_name = "Meta" |
3545 | + Meta_bases = (ConfigMetaData,) |
3546 | + Meta_ns = { |
3547 | + 'variable_list': variable_list, |
3548 | + 'section_list': section_list |
3549 | + } |
3550 | + if 'Meta' in namespace: |
3551 | + user_Meta_cls = namespace['Meta'] |
3552 | + if not isinstance(user_Meta_cls, type): |
3553 | + raise TypeError("Meta must be a class") |
3554 | + Meta_bases = (user_Meta_cls, ConfigMetaData) |
3555 | + # Create a new type for the Meta class |
3556 | + namespace['Meta'] = type.__new__( |
3557 | + type(ConfigMetaData), Meta_name, Meta_bases, Meta_ns) |
3558 | + # Create a new type for the Config subclass |
3559 | + return type.__new__(mcls, name, bases, namespace) |
3560 | + |
3561 | + @classmethod |
3562 | + def __prepare__(mcls, name, bases, **kwargs): |
3563 | + return collections.OrderedDict() |
3564 | + |
3565 | + |
3566 | +class PlainBoxConfigParser(configparser.ConfigParser): |
3567 | + """ |
3568 | + A simple ConfigParser subclass that does not lowercase |
3569 | + key names. |
3570 | + """ |
3571 | + def optionxform(self, option): |
3572 | + return option |
3573 | + |
3574 | + |
3575 | +class Config(metaclass=ConfigMeta): |
3576 | + """ |
3577 | + Base class for configuration systems |
3578 | + |
3579 | + :ivar _var: |
3580 | + storage backend for Variable definitions |
3581 | + |
3582 | + :ivar _section: |
3583 | + storage backend for Section definitions |
3584 | + |
3585 | + :ivar _filename_list: |
3586 | + list of pathnames to files that were loaded by the last call to |
3587 | + :meth:`read()` |
3588 | + |
3589 | + :ivar _problem_list: |
3590 | + list of :class:`ValidationError` that were detected by the last call to |
3591 | + :meth:`read()` |
3592 | + """ |
3593 | + |
3594 | + def __init__(self): |
3595 | + """ |
3596 | + Initialize an empty Config object |
3597 | + """ |
3598 | + self._var = {} |
3599 | + self._section = {} |
3600 | + self._filename_list = [] |
3601 | + self._problem_list = [] |
3602 | + |
3603 | + @property |
3604 | + def problem_list(self): |
3605 | + """ |
3606 | + list of :class:`ValidationError` that were detected by the last call to |
3607 | + :meth:`read()` |
3608 | + """ |
3609 | + return self._problem_list |
3610 | + |
3611 | + @property |
3612 | + def filename_list(self): |
3613 | + """ |
3614 | + list of pathnames to files that were loaded by the last call to |
3615 | + :meth:`read()` |
3616 | + """ |
3617 | + return self._filename_list |
3618 | + |
3619 | + @classmethod |
3620 | + def get(cls): |
3621 | + """ |
3622 | + Get an instance of this Config class with all the configuration loaded |
3623 | + from default locations. The locations are determined by |
3624 | + Meta.filename_list attribute. |
3625 | + |
3626 | + :returns: fresh :class:`Config` instance |
3627 | + |
3628 | + """ |
3629 | + self = cls() |
3630 | + self.read(cls.Meta.filename_list) |
3631 | + return self |
3632 | + |
3633 | + def read(self, filename_list): |
3634 | + """ |
3635 | + Load and merge settings from many files. |
3636 | + |
3637 | + This method tries to open each file from the list of filenames, parse |
3638 | + it as an INI file using :class:`PlainBoxConfigParser` (a simple |
3639 | + ConfigParser subclass that respects the case of key names). The list of |
3640 | + files actually accessed is saved as available as |
3641 | + :attr:`Config.filename_list`. |
3642 | + |
3643 | + If any problem is detected during parsing (e.g. syntax errors) those |
3644 | + are captured and added to the :attr:`Config.problem_list`. |
3645 | + |
3646 | + After all files are loaded each :class:`Variable` and :class:`Section` |
3647 | + defined in the :class:`Config` class is assigned with the data from the |
3648 | + merged configuration data. |
3649 | + |
3650 | + Any variables that cannot be assigned and raise |
3651 | + :class:`ValidationError` are ignored but the list of problems is saved. |
3652 | + |
3653 | + All unused configuration (extra variables that are not defined as |
3654 | + either Variable or Section class) is silently ignored. |
3655 | + |
3656 | + .. note:: |
3657 | + This method resets :ivar:`_problem_list` and |
3658 | + :ivar:`_filename_list`. |
3659 | + """ |
3660 | + parser = PlainBoxConfigParser() |
3661 | + # Reset filename list and problem list |
3662 | + self._filename_list = [] |
3663 | + self._problem_list = [] |
3664 | + # Try loading all of the config files |
3665 | + try: |
3666 | + self._filename_list = parser.read(filename_list) |
3667 | + except configparser.Error as exc: |
3668 | + self._problem_list.append(exc) |
3669 | + # Pick a reader function appropriate for the kind of variable |
3670 | + reader_fn = { |
3671 | + str: parser.get, |
3672 | + bool: parser.getboolean, |
3673 | + int: parser.getint, |
3674 | + float: parser.getfloat |
3675 | + } |
3676 | + # Load all variables that we know about |
3677 | + for variable in self.Meta.variable_list: |
3678 | + # Access the variable in the configuration file |
3679 | + try: |
3680 | + value = reader_fn[variable.kind]( |
3681 | + variable.section, variable.name) |
3682 | + except configparser.NoSectionError: |
3683 | + continue |
3684 | + except configparser.NoOptionError: |
3685 | + continue |
3686 | + # Try to assign it |
3687 | + try: |
3688 | + variable.__set__(self, value) |
3689 | + except ValidationError as exc: |
3690 | + self.problem_list.append(exc) |
3691 | + # Load all sections that we know about |
3692 | + for section in self.Meta.section_list: |
3693 | + try: |
3694 | + # Access the section in the configuration file |
3695 | + value = dict(parser.items(section.name)) |
3696 | + except configparser.NoSectionError: |
3697 | + continue |
3698 | + # Assign it |
3699 | + section.__set__(self, value) |
3700 | + |
3701 | + def _get_variable(self, name): |
3702 | + """ |
3703 | + Internal method called by :meth:`Variable.__get__` |
3704 | + """ |
3705 | + return self._var[name] |
3706 | + |
3707 | + def _set_variable(self, name, value): |
3708 | + """ |
3709 | + Internal method called by :meth:`Variable.__set__` |
3710 | + """ |
3711 | + self._var[name] = value |
3712 | + |
3713 | + def _del_variable(self, name): |
3714 | + """ |
3715 | + Internal method called by :meth:`Variable.__delete__` |
3716 | + """ |
3717 | + del self._var[name] |
3718 | + |
3719 | + def _get_section(self, name): |
3720 | + """ |
3721 | + Internal method called by :meth:`Section.__get__` |
3722 | + """ |
3723 | + return self._section[name] |
3724 | + |
3725 | + def _set_section(self, name, value): |
3726 | + """ |
3727 | + Internal method called by :meth:`Section.__set__` |
3728 | + """ |
3729 | + self._section[name] = value |
3730 | + |
3731 | + def _del_section(self, name): |
3732 | + """ |
3733 | + Internal method called by :meth:`Section.__delete__` |
3734 | + """ |
3735 | + del self._section[name] |
3736 | + |
3737 | + |
3738 | +class ValidationError(ValueError): |
3739 | + """ |
3740 | + Exception raised when configuration variables fail to validate |
3741 | + """ |
3742 | + |
3743 | + def __init__(self, variable, new_value, message): |
3744 | + self.variable = variable |
3745 | + self.new_value = new_value |
3746 | + self.message = message |
3747 | + |
3748 | + def __str__(self): |
3749 | + return self.message |
3750 | + |
3751 | + |
3752 | +class IValidator(metaclass=ABCMeta): |
3753 | + """ |
3754 | + An interface for variable vale validators |
3755 | + """ |
3756 | + |
3757 | + @abstractmethod |
3758 | + def __call__(self, variable, new_value): |
3759 | + """ |
3760 | + Check if a value is appropriate for the variable. |
3761 | + |
3762 | + :returns: None if everything is okay |
3763 | + :returns: string that describes the problem if the value cannot be used |
3764 | + """ |
3765 | + |
3766 | + |
3767 | +def KindValidator(variable, new_value): |
3768 | + """ |
3769 | + A validator ensuring that values match the "kind" of the variable. |
3770 | + """ |
3771 | + if not isinstance(new_value, variable.kind): |
3772 | + return "expected a {}".format(variable.kind.__name__) |
3773 | + |
3774 | + |
3775 | +class PatternValidator(IValidator): |
3776 | + """ |
3777 | + A validator ensuring that values match a given pattern |
3778 | + """ |
3779 | + |
3780 | + def __init__(self, pattern_text): |
3781 | + self.pattern_text = pattern_text |
3782 | + self.pattern = re.compile(pattern_text) |
3783 | + |
3784 | + def __call__(self, variable, new_value): |
3785 | + if not self.pattern.match(new_value): |
3786 | + return "does not match pattern: {!r}".format(self.pattern_text) |
3787 | |
3788 | === added file 'plainbox/plainbox/impl/job.py.OTHER' |
3789 | --- plainbox/plainbox/impl/job.py.OTHER 1970-01-01 00:00:00 +0000 |
3790 | +++ plainbox/plainbox/impl/job.py.OTHER 2013-08-28 12:37:27 +0000 |
3791 | @@ -0,0 +1,259 @@ |
3792 | +# This file is part of Checkbox. |
3793 | +# |
3794 | +# Copyright 2012, 2013 Canonical Ltd. |
3795 | +# Written by: |
3796 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
3797 | +# |
3798 | +# Checkbox is free software: you can redistribute it and/or modify |
3799 | +# it under the terms of the GNU General Public License as published by |
3800 | +# the Free Software Foundation, either version 3 of the License, or |
3801 | +# (at your option) any later version. |
3802 | +# |
3803 | +# Checkbox is distributed in the hope that it will be useful, |
3804 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3805 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3806 | +# GNU General Public License for more details. |
3807 | +# |
3808 | +# You should have received a copy of the GNU General Public License |
3809 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
3810 | + |
3811 | +""" |
3812 | +:mod:`plainbox.impl.job` -- job definition |
3813 | +========================================== |
3814 | + |
3815 | +.. warning:: |
3816 | + |
3817 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
3818 | +""" |
3819 | + |
3820 | +import logging |
3821 | +import os |
3822 | +import re |
3823 | + |
3824 | +from plainbox.abc import IJobDefinition |
3825 | +from plainbox.impl.config import Unset |
3826 | +from plainbox.impl.resource import ResourceProgram |
3827 | +from plainbox.impl.secure.checkbox_trusted_launcher import BaseJob |
3828 | + |
3829 | + |
3830 | +logger = logging.getLogger("plainbox.job") |
3831 | + |
3832 | + |
3833 | +class JobDefinition(BaseJob, IJobDefinition): |
3834 | + """ |
3835 | + Job definition class. |
3836 | + |
3837 | + Thin wrapper around the RFC822 record that defines a checkbox job |
3838 | + definition |
3839 | + """ |
3840 | + |
3841 | + @property |
3842 | + def name(self): |
3843 | + return self.__getattr__('name') |
3844 | + |
3845 | + @property |
3846 | + def requires(self): |
3847 | + try: |
3848 | + return self.__getattr__('requires') |
3849 | + except AttributeError: |
3850 | + return None |
3851 | + |
3852 | + @property |
3853 | + def description(self): |
3854 | + try: |
3855 | + return self.__getattr__('description') |
3856 | + except AttributeError: |
3857 | + return None |
3858 | + |
3859 | + @property |
3860 | + def depends(self): |
3861 | + try: |
3862 | + return self.__getattr__('depends') |
3863 | + except AttributeError: |
3864 | + return None |
3865 | + |
3866 | + @property |
3867 | + def via(self): |
3868 | + """ |
3869 | + The checksum of the "parent" job when the current JobDefinition comes |
3870 | + from a job output using the local plugin |
3871 | + """ |
3872 | + return self._via |
3873 | + |
3874 | + @property |
3875 | + def origin(self): |
3876 | + """ |
3877 | + The Origin object associated with this JobDefinition |
3878 | + |
3879 | + May be None |
3880 | + """ |
3881 | + return self._origin |
3882 | + |
3883 | + def __init__(self, data, origin=None, checkbox=None, via=None): |
3884 | + super(JobDefinition, self).__init__(data) |
3885 | + self._resource_program = None |
3886 | + self._origin = origin |
3887 | + self._checkbox = checkbox |
3888 | + self._via = via |
3889 | + |
3890 | + def __str__(self): |
3891 | + return self.name |
3892 | + |
3893 | + def __repr__(self): |
3894 | + return "<JobDefinition name:{!r} plugin:{!r}>".format( |
3895 | + self.name, self.plugin) |
3896 | + |
3897 | + def __getattr__(self, attr): |
3898 | + if attr in self._data: |
3899 | + return self._data[attr] |
3900 | + gettext_attr = "_{}".format(attr) |
3901 | + if gettext_attr in self._data: |
3902 | + value = self._data[gettext_attr] |
3903 | + # TODO: feed through gettext |
3904 | + return value |
3905 | + raise AttributeError(attr) |
3906 | + |
3907 | + def _get_persistance_subset(self): |
3908 | + state = {} |
3909 | + state['data'] = {} |
3910 | + for key, value in self._data.items(): |
3911 | + state['data'][key] = value |
3912 | + if self.via is not None: |
3913 | + state['via'] = self.via |
3914 | + return state |
3915 | + |
3916 | + def __eq__(self, other): |
3917 | + if not isinstance(other, JobDefinition): |
3918 | + return False |
3919 | + return self.get_checksum() == other.get_checksum() |
3920 | + |
3921 | + def __ne__(self, other): |
3922 | + if not isinstance(other, JobDefinition): |
3923 | + return True |
3924 | + return self.get_checksum() != other.get_checksum() |
3925 | + |
3926 | + def get_resource_program(self): |
3927 | + """ |
3928 | + Return a ResourceProgram based on the 'requires' expression. |
3929 | + |
3930 | + The program instance is cached in the JobDefinition and is not |
3931 | + compiled or validated on subsequent calls. |
3932 | + |
3933 | + Returns ResourceProgram or None |
3934 | + Raises ResourceProgramError or SyntaxError |
3935 | + """ |
3936 | + if self.requires is not None and self._resource_program is None: |
3937 | + self._resource_program = ResourceProgram(self.requires) |
3938 | + return self._resource_program |
3939 | + |
3940 | + def get_direct_dependencies(self): |
3941 | + """ |
3942 | + Compute and return a set of direct dependencies |
3943 | + |
3944 | + To combat a simple mistake where the jobs are space-delimited any |
3945 | + mixture of white-space (including newlines) and commas are allowed. |
3946 | + """ |
3947 | + if self.depends: |
3948 | + return {name for name in re.split('[\s,]+', self.depends)} |
3949 | + else: |
3950 | + return set() |
3951 | + |
3952 | + def get_resource_dependencies(self): |
3953 | + """ |
3954 | + Compute and return a set of resource dependencies |
3955 | + """ |
3956 | + program = self.get_resource_program() |
3957 | + if program: |
3958 | + return program.required_resources |
3959 | + else: |
3960 | + return set() |
3961 | + |
3962 | + @classmethod |
3963 | + def from_rfc822_record(cls, record): |
3964 | + """ |
3965 | + Create a JobDefinition instance from rfc822 record |
3966 | + |
3967 | + The record must be a RFC822Record instance. |
3968 | + |
3969 | + Only the 'name' and 'plugin' keys are required. |
3970 | + All other data is stored as is and is entirely optional. |
3971 | + """ |
3972 | + for key in ['plugin', 'name']: |
3973 | + if key not in record.data: |
3974 | + raise ValueError( |
3975 | + "Required record key {!r} was not found".format(key)) |
3976 | + return cls(record.data, record.origin) |
3977 | + |
3978 | + def modify_execution_environment(self, env, session_dir, config=None): |
3979 | + """ |
3980 | + Alter execution environment as required to execute this job. |
3981 | + |
3982 | + The environment is modified in place. |
3983 | + |
3984 | + The session_dir argument can be passed to scripts to know where to |
3985 | + create temporary data. This data will persist during the lifetime of |
3986 | + the session. |
3987 | + |
3988 | + The config argument (which defaults to None) should be a PlainBoxConfig |
3989 | + object. It is used to provide values for missing environment variables |
3990 | + that are required by the job (as expressed by the environ key in the |
3991 | + job definition file). |
3992 | + |
3993 | + Computes and modifies the dictionary of additional values that need to |
3994 | + be added to the base environment. Note that all changes to the |
3995 | + environment (modifications, not replacements) depend on the current |
3996 | + environment. This may be of importance when attempting to setup the |
3997 | + test session as another user. |
3998 | + |
3999 | + This environment has additional PATH, PYTHONPATH entries. It also uses |
4000 | + fixed LANG so that scripts behave as expected. Lastly it sets |
4001 | + CHECKBOX_SHARE that is required by some scripts. |
4002 | + """ |
4003 | + # XXX: this obviously requires a checkbox object to know where stuff is |
4004 | + # but during the transition we may not have one available. |
4005 | + assert self._checkbox is not None |
4006 | + # Use PATH that can lookup checkbox scripts |
4007 | + if self._checkbox.extra_PYTHONPATH: |
4008 | + env['PYTHONPATH'] = os.pathsep.join( |
4009 | + [self._checkbox.extra_PYTHONPATH] |
4010 | + + env.get("PYTHONPATH", "").split(os.pathsep)) |
4011 | + # Update PATH so that scripts can be found |
4012 | + env['PATH'] = os.pathsep.join( |
4013 | + [self._checkbox.extra_PATH] |
4014 | + + env.get("PATH", "").split(os.pathsep)) |
4015 | + # Add CHECKBOX_SHARE that is needed by one script |
4016 | + env['CHECKBOX_SHARE'] = self._checkbox.CHECKBOX_SHARE |
4017 | + # Add CHECKBOX_DATA (temporary checkbox data) |
4018 | + env['CHECKBOX_DATA'] = session_dir |
4019 | + # Inject additional variables that are requested in the config |
4020 | + if config is not None and config.environment is not Unset: |
4021 | + for env_var in config.environment: |
4022 | + # Don't override anything that is already present in the |
4023 | + # current environment. This will allow users to customize |
4024 | + # variables without editing any config files. |
4025 | + if env_var in env: |
4026 | + continue |
4027 | + # If the environment section of the configuration file has a |
4028 | + # particular variable then copy it over. |
4029 | + env[env_var] = config.environment[env_var] |
4030 | + |
4031 | + def create_child_job_from_record(self, record): |
4032 | + """ |
4033 | + Create a new JobDefinition from RFC822 record. |
4034 | + |
4035 | + This method should only be used to create additional jobs from local |
4036 | + jobs (plugin local). The intent is two-fold: |
4037 | + 1) to encapsulate the sharing of the embedded checkbox reference. |
4038 | + 2) to set the ``via`` attribute (to aid the trusted launcher) |
4039 | + """ |
4040 | + job = self.from_rfc822_record(record) |
4041 | + job._checkbox = self._checkbox |
4042 | + job._via = self.get_checksum() |
4043 | + return job |
4044 | + |
4045 | + @classmethod |
4046 | + def from_json_record(cls, record): |
4047 | + """ |
4048 | + Create a JobDefinition instance from JSON record |
4049 | + """ |
4050 | + return cls(record['data'], via=record.get('via')) |
4051 | |
4052 | === added file 'plainbox/plainbox/impl/runner.py.OTHER' |
4053 | --- plainbox/plainbox/impl/runner.py.OTHER 1970-01-01 00:00:00 +0000 |
4054 | +++ plainbox/plainbox/impl/runner.py.OTHER 2013-08-28 12:37:27 +0000 |
4055 | @@ -0,0 +1,421 @@ |
4056 | +# This file is part of Checkbox. |
4057 | +# |
4058 | +# Copyright 2012 Canonical Ltd. |
4059 | +# Written by: |
4060 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
4061 | +# |
4062 | +# Checkbox is free software: you can redistribute it and/or modify |
4063 | +# it under the terms of the GNU General Public License as published by |
4064 | +# the Free Software Foundation, either version 3 of the License, or |
4065 | +# (at your option) any later version. |
4066 | +# |
4067 | +# Checkbox is distributed in the hope that it will be useful, |
4068 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4069 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4070 | +# GNU General Public License for more details. |
4071 | +# |
4072 | +# You should have received a copy of the GNU General Public License |
4073 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
4074 | + |
4075 | +""" |
4076 | +:mod:`plainbox.impl.runner` -- job runner |
4077 | +========================================= |
4078 | + |
4079 | +.. warning:: |
4080 | + |
4081 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
4082 | +""" |
4083 | + |
4084 | +import collections |
4085 | +import datetime |
4086 | +import json |
4087 | +import logging |
4088 | +import os |
4089 | +import string |
4090 | + |
4091 | +from plainbox.vendor import extcmd |
4092 | + |
4093 | +from plainbox.abc import IJobRunner |
4094 | +from plainbox.impl.result import JobResult, IOLogRecord, IoLogEncoder |
4095 | + |
4096 | +logger = logging.getLogger("plainbox.runner") |
4097 | + |
4098 | + |
4099 | +def slugify(_string): |
4100 | + """ |
4101 | + Slugify - like Django does for URL - transform a random string to a valid |
4102 | + slug that can be later used in filenames |
4103 | + """ |
4104 | + valid_chars = frozenset( |
4105 | + "-_.{}{}".format(string.ascii_letters, string.digits)) |
4106 | + return ''.join(c if c in valid_chars else '_' for c in _string) |
4107 | + |
4108 | + |
4109 | +def io_log_write(log, stream): |
4110 | + """ |
4111 | + JSON call to serialize io_log objects to disk |
4112 | + """ |
4113 | + json.dump( |
4114 | + log, stream, ensure_ascii=False, indent=None, cls=IoLogEncoder, |
4115 | + separators=(',', ':')) |
4116 | + |
4117 | + |
4118 | +def authenticate_warmup(): |
4119 | + """ |
4120 | + Call the checkbox trusted launcher in warmup mode. |
4121 | + |
4122 | + This will use the corresponding PolicyKit action and start the |
4123 | + authentication agent (depending on the installed policy file) |
4124 | + """ |
4125 | + warmup_popen = extcmd.ExternalCommand() |
4126 | + return warmup_popen.call( |
4127 | + ['pkexec', 'checkbox-trusted-launcher', '--warmup']) |
4128 | + |
4129 | + |
4130 | +class CommandIOLogBuilder(extcmd.DelegateBase): |
4131 | + """ |
4132 | + Delegate for extcmd that builds io_log entries. |
4133 | + |
4134 | + IO log entries are records kept by JobResult.io_log and correspond to all |
4135 | + of the data that was written by called process. The format is a sequence of |
4136 | + tuples (delay, stream_name, data). |
4137 | + """ |
4138 | + |
4139 | + def on_begin(self, args, kwargs): |
4140 | + """ |
4141 | + Internal method of extcmd.DelegateBase |
4142 | + |
4143 | + Called when a command is being invoked. |
4144 | + Begins tracking time (relative time entries) and creates the empty |
4145 | + io_log list. |
4146 | + """ |
4147 | + logger.debug("io log starting for command: %r", args) |
4148 | + self.io_log = [] |
4149 | + self.last_msg = datetime.datetime.utcnow() |
4150 | + |
4151 | + def on_line(self, stream_name, line): |
4152 | + """ |
4153 | + Internal method of IOLogBuilder |
4154 | + |
4155 | + Appends each line to the io_log. Maintains a timestamp of the last |
4156 | + message so that approximate delay between each piece of output can be |
4157 | + recorded as well. |
4158 | + """ |
4159 | + now = datetime.datetime.utcnow() |
4160 | + delay = now - self.last_msg |
4161 | + self.last_msg = now |
4162 | + record = IOLogRecord(delay.total_seconds(), stream_name, line) |
4163 | + self.io_log.append(record) |
4164 | + logger.debug("io log captured %r", record) |
4165 | + |
4166 | + |
4167 | +class CommandOutputWriter(extcmd.DelegateBase): |
4168 | + """ |
4169 | + Delegate for extcmd that writes output to a file on disk. |
4170 | + |
4171 | + The file itself is only opened once on_begin() gets called by extcmd. This |
4172 | + makes it safe to instantiate this without worrying about dangling |
4173 | + resources. |
4174 | + """ |
4175 | + |
4176 | + def __init__(self, stdout_path, stderr_path): |
4177 | + """ |
4178 | + Initialize new writer. |
4179 | + |
4180 | + Just records output paths. |
4181 | + """ |
4182 | + self.stdout_path = stdout_path |
4183 | + self.stderr_path = stderr_path |
4184 | + |
4185 | + def on_begin(self, args, kwargs): |
4186 | + """ |
4187 | + Internal method of extcmd.DelegateBase |
4188 | + |
4189 | + Called when a command is being invoked |
4190 | + """ |
4191 | + self.stdout = open(self.stdout_path, "wb") |
4192 | + self.stderr = open(self.stderr_path, "wb") |
4193 | + |
4194 | + def on_end(self, returncode): |
4195 | + """ |
4196 | + Internal method of extcmd.DelegateBase |
4197 | + |
4198 | + Called when a command finishes running |
4199 | + """ |
4200 | + self.stdout.close() |
4201 | + self.stderr.close() |
4202 | + |
4203 | + def on_line(self, stream_name, line): |
4204 | + """ |
4205 | + Internal method of extcmd.DelegateBase |
4206 | + |
4207 | + Called for each line of output. |
4208 | + """ |
4209 | + if stream_name == 'stdout': |
4210 | + self.stdout.write(line) |
4211 | + elif stream_name == 'stderr': |
4212 | + self.stderr.write(line) |
4213 | + |
4214 | + |
4215 | +class FallbackCommandOutputPrinter(extcmd.DelegateBase): |
4216 | + """ |
4217 | + Delegate for extcmd that prints all output to stdout. |
4218 | + |
4219 | + This delegate is only used as a fallback when no delegate was explicitly |
4220 | + provided to a JobRunner instance. |
4221 | + """ |
4222 | + |
4223 | + def __init__(self, prompt): |
4224 | + self._prompt = prompt |
4225 | + self._lineno = collections.defaultdict(int) |
4226 | + self._abort = False |
4227 | + |
4228 | + def on_line(self, stream_name, line): |
4229 | + if self._abort: |
4230 | + return |
4231 | + self._lineno[stream_name] += 1 |
4232 | + try: |
4233 | + print("(job {}, <{}:{:05}>) {}".format( |
4234 | + self._prompt, stream_name, self._lineno[stream_name], |
4235 | + line.decode('UTF-8').rstrip())) |
4236 | + except UnicodeDecodeError: |
4237 | + self._abort = True |
4238 | + |
4239 | + |
4240 | +class JobRunner(IJobRunner): |
4241 | + """ |
4242 | + Runner for jobs - executes jobs and produces results |
4243 | + |
4244 | + The runner is somewhat de-coupled from jobs and session. It still carries |
4245 | + all checkbox-specific logic about the various types of plugins. |
4246 | + |
4247 | + The runner consumes jobs and configuration objects and produces job result |
4248 | + objects. The runner can operate in dry-run mode, when enabled, most jobs |
4249 | + are never started. Only jobs listed in DRY_RUN_PLUGINS are executed. |
4250 | + """ |
4251 | + |
4252 | + # List of plugins that are still executed |
4253 | + _DRY_RUN_PLUGINS = ('local', 'resource', 'attachment') |
4254 | + |
4255 | + def __init__(self, session_dir, jobs_io_log_dir, |
4256 | + command_io_delegate=None, outcome_callback=None, |
4257 | + dry_run=False): |
4258 | + """ |
4259 | + Initialize a new job runner. |
4260 | + |
4261 | + Uses the specified session_dir as CHECKBOX_DATA environment variable. |
4262 | + Uses the specified IO delegate for extcmd.ExternalCommandWithDelegate |
4263 | + to track IO done by the called commands (optional, a simple console |
4264 | + printer is provided if missing). |
4265 | + """ |
4266 | + self._session_dir = session_dir |
4267 | + self._jobs_io_log_dir = jobs_io_log_dir |
4268 | + self._command_io_delegate = command_io_delegate |
4269 | + self._outcome_callback = outcome_callback |
4270 | + self._dry_run = dry_run |
4271 | + |
4272 | + def run_job(self, job, config=None): |
4273 | + """ |
4274 | + Run the specified job an return the result |
4275 | + """ |
4276 | + logger.info("Running %r", job) |
4277 | + func_name = "_plugin_" + job.plugin.replace('-', '_') |
4278 | + try: |
4279 | + runner = getattr(self, func_name) |
4280 | + except AttributeError: |
4281 | + return JobResult({ |
4282 | + 'job': job, |
4283 | + 'outcome': JobResult.OUTCOME_NOT_IMPLEMENTED, |
4284 | + 'comment': 'This plugin is not supported' |
4285 | + }) |
4286 | + else: |
4287 | + if self._dry_run and job.plugin not in self._DRY_RUN_PLUGINS: |
4288 | + return self._dry_run_result(job) |
4289 | + else: |
4290 | + return runner(job, config) |
4291 | + |
4292 | + def _dry_run_result(self, job): |
4293 | + """ |
4294 | + Produce the result that is used when running in dry-run mode |
4295 | + """ |
4296 | + return JobResult({ |
4297 | + 'job': job, |
4298 | + 'outcome': JobResult.OUTCOME_SKIP, |
4299 | + 'comments': "Job skipped in dry-run mode" |
4300 | + }) |
4301 | + |
4302 | + def _plugin_shell(self, job, config): |
4303 | + return self._just_run_command(job, config) |
4304 | + |
4305 | + _plugin_attachment = _plugin_shell |
4306 | + |
4307 | + def _plugin_resource(self, job, config): |
4308 | + return self._just_run_command(job, config) |
4309 | + |
4310 | + def _plugin_local(self, job, config): |
4311 | + return self._just_run_command(job, config) |
4312 | + |
4313 | + def _plugin_manual(self, job, config): |
4314 | + if self._outcome_callback is None: |
4315 | + return JobResult({ |
4316 | + 'job': job, |
4317 | + 'outcome': JobResult.OUTCOME_SKIP, |
4318 | + 'comment': "non-interactive test run" |
4319 | + }) |
4320 | + else: |
4321 | + result = self._just_run_command(job, config) |
4322 | + # XXX: make outcome writable |
4323 | + result._data['outcome'] = self._outcome_callback() |
4324 | + return result |
4325 | + |
4326 | + _plugin_user_interact = _plugin_manual |
4327 | + _plugin_user_verify = _plugin_manual |
4328 | + |
4329 | + def _just_run_command(self, job, config): |
4330 | + # Run the embedded command |
4331 | + return_code, io_log = self._run_command(job, config) |
4332 | + # Convert the return of the command to the outcome of the job |
4333 | + if return_code == 0: |
4334 | + outcome = JobResult.OUTCOME_PASS |
4335 | + else: |
4336 | + outcome = JobResult.OUTCOME_FAIL |
4337 | + # Create a result object and return it |
4338 | + return JobResult({ |
4339 | + 'job': job, |
4340 | + 'outcome': outcome, |
4341 | + 'return_code': return_code, |
4342 | + 'io_log': io_log |
4343 | + }) |
4344 | + |
4345 | + def _get_script_env(self, job, config=None, only_changes=False): |
4346 | + """ |
4347 | + Compute the environment the script will be executed in |
4348 | + """ |
4349 | + # Get a proper environment |
4350 | + env = dict(os.environ) |
4351 | + # Use non-internationalized environment |
4352 | + env['LANG'] = 'C.UTF-8' |
4353 | + # Allow the job to customize anything |
4354 | + job.modify_execution_environment(env, self._session_dir, config) |
4355 | + # If a differential environment is requested return only the subset |
4356 | + # that has been altered. |
4357 | + # |
4358 | + # XXX: This will effectively give the root user our PATH which _may_ be |
4359 | + # good bud _might_ be dangerous. This will need some peer review. |
4360 | + if only_changes: |
4361 | + return {key: value |
4362 | + for key, value in env.items() |
4363 | + if key not in os.environ or os.environ[key] != value |
4364 | + or key in job.get_environ_settings()} |
4365 | + else: |
4366 | + return env |
4367 | + |
4368 | + def _get_command_trusted(self, job, config=None): |
4369 | + # When the job requires to run as root then elevate our permissions |
4370 | + # via pkexec(1). Since pkexec resets environment we need to somehow |
4371 | + # pass the extra things we require. To do that we pass the list of |
4372 | + # changed environment variables in addition to the job hash. |
4373 | + cmd = ['checkbox-trusted-launcher', '--hash', job.get_checksum()] + [ |
4374 | + "{key}={value}".format(key=key, value=value) |
4375 | + for key, value in self._get_script_env( |
4376 | + job, config, only_changes=True |
4377 | + ).items() |
4378 | + ] |
4379 | + if job.via is not None: |
4380 | + cmd += ['--via', job.via] |
4381 | + return cmd |
4382 | + |
4383 | + def _get_command_src(self, job, config=None): |
4384 | + # Running PlainBox from source doesn't require the trusted launcher |
4385 | + # That's why we use the env(1)' command and pass it the list of |
4386 | + # changed environment variables. |
4387 | + # The whole pkexec and env part gets prepended to the command |
4388 | + # we were supposed to run. |
4389 | + cmd = ['env'] |
4390 | + cmd += [ |
4391 | + "{key}={value}".format(key=key, value=value) |
4392 | + for key, value in self._get_script_env( |
4393 | + job, only_changes=True |
4394 | + ).items() |
4395 | + ] |
4396 | + cmd += ['bash', '-c', job.command] |
4397 | + return cmd |
4398 | + |
4399 | + def _run_command(self, job, config): |
4400 | + """ |
4401 | + Run the shell command associated with the specified job. |
4402 | + |
4403 | + Returns a tuple (return_code, io_log) |
4404 | + """ |
4405 | + # Bail early if there is nothing do do |
4406 | + if job.command is None: |
4407 | + return None, () |
4408 | + ui_io_delegate = self._command_io_delegate |
4409 | + # If there is no UI delegate specified create a simple |
4410 | + # delegate that logs all output to the console |
4411 | + if ui_io_delegate is None: |
4412 | + ui_io_delegate = FallbackCommandOutputPrinter(job.name) |
4413 | + # Create a delegate that writes all IO to disk |
4414 | + slug = slugify(job.name) |
4415 | + output_writer = CommandOutputWriter( |
4416 | + stdout_path=os.path.join(self._jobs_io_log_dir, |
4417 | + "{}.stdout".format(slug)), |
4418 | + stderr_path=os.path.join(self._jobs_io_log_dir, |
4419 | + "{}.stderr".format(slug))) |
4420 | + # Create a delegate that builds a log of all IO |
4421 | + io_log_builder = CommandIOLogBuilder() |
4422 | + # Create the delegate for routing IO |
4423 | + # |
4424 | + # |
4425 | + # Split the stream of data into three parts (each part is expressed as |
4426 | + # an element of extcmd.Chain()). |
4427 | + # |
4428 | + # Send the first copy of the data through bytes->text decoder and |
4429 | + # then to the UI delegate. This cold be something provided by the |
4430 | + # higher level caller or the default CommandOutputLogger. |
4431 | + # |
4432 | + # Send the second copy of the data to the _IOLogBuilder() instance that |
4433 | + # just concatenates subsequent bytes into neat time-stamped records. |
4434 | + # |
4435 | + # Send the third copy to the output writer that writes everything to |
4436 | + # disk. |
4437 | + delegate = extcmd.Chain([ |
4438 | + ui_io_delegate, |
4439 | + io_log_builder, |
4440 | + output_writer]) |
4441 | + logger.debug("job[%s] extcmd delegate: %r", job.name, delegate) |
4442 | + # Create a subprocess.Popen() like object that uses the delegate |
4443 | + # system to observe all IO as it occurs in real time. |
4444 | + logging_popen = extcmd.ExternalCommandWithDelegate(delegate) |
4445 | + # Start the process and wait for it to finish getting the |
4446 | + # result code. This will actually call a number of callbacks |
4447 | + # while the process is running. It will also spawn a few |
4448 | + # threads although all callbacks will be fired from a single |
4449 | + # thread (which is _not_ the main thread) |
4450 | + logger.debug("job[%s] starting command: %s", job.name, job.command) |
4451 | + if job.user is not None: |
4452 | + if job._checkbox._mode == 'src': |
4453 | + cmd = self._get_command_src(job, config) |
4454 | + else: |
4455 | + cmd = self._get_command_trusted(job, config) |
4456 | + cmd = ['pkexec', '--user', job.user] + cmd |
4457 | + logging.debug("job[%s] executing %r", job.name, cmd) |
4458 | + return_code = logging_popen.call(cmd) |
4459 | + else: |
4460 | + # XXX: sadly using /bin/sh results in broken output |
4461 | + # XXX: maybe run it both ways and raise exceptions on differences? |
4462 | + cmd = ['bash', '-c', job.command] |
4463 | + logging.debug("job[%s] executing %r", job.name, cmd) |
4464 | + return_code = logging_popen.call( |
4465 | + cmd, env=self._get_script_env(job, config)) |
4466 | + logger.debug("job[%s] command return code: %r", |
4467 | + job.name, return_code) |
4468 | + # XXX: Perhaps handle process dying from signals here |
4469 | + # When the process is killed proc.returncode is not set |
4470 | + # and another (cannot remember now) attribute is set |
4471 | + fjson = os.path.join(self._jobs_io_log_dir, "{}.json".format(slug)) |
4472 | + with open(fjson, "wt") as stream: |
4473 | + io_log_write(io_log_builder.io_log, stream) |
4474 | + stream.flush() |
4475 | + os.fsync(stream.fileno()) |
4476 | + return return_code, fjson |
4477 | |
4478 | === added directory 'plainbox/plainbox/impl/secure' |
4479 | === added file 'plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py.OTHER' |
4480 | --- plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py.OTHER 1970-01-01 00:00:00 +0000 |
4481 | +++ plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py.OTHER 2013-08-28 12:37:27 +0000 |
4482 | @@ -0,0 +1,395 @@ |
4483 | +# This file is part of Checkbox. |
4484 | +# |
4485 | +# Copyright 2013 Canonical Ltd. |
4486 | +# Written by: |
4487 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
4488 | +# |
4489 | +# Checkbox is free software: you can redistribute it and/or modify |
4490 | +# it under the terms of the GNU General Public License as published by |
4491 | +# the Free Software Foundation, either version 3 of the License, or |
4492 | +# (at your option) any later version. |
4493 | +# |
4494 | +# Checkbox is distributed in the hope that it will be useful, |
4495 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4496 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4497 | +# GNU General Public License for more details. |
4498 | +# |
4499 | +# You should have received a copy of the GNU General Public License |
4500 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
4501 | + |
4502 | +""" |
4503 | +:mod:`plainbox.impl.secure.checkbox_trusted_launcher` -- command launcher |
4504 | +========================================================================= |
4505 | + |
4506 | +.. warning:: |
4507 | + |
4508 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
4509 | +""" |
4510 | + |
4511 | +import argparse |
4512 | +import collections |
4513 | +import glob |
4514 | +import hashlib |
4515 | +import json |
4516 | +import os |
4517 | +import re |
4518 | +import subprocess |
4519 | +from inspect import cleandoc |
4520 | + |
4521 | + |
4522 | +class BaseJob: |
4523 | + """ |
4524 | + Base Job definition class. |
4525 | + """ |
4526 | + |
4527 | + @property |
4528 | + def plugin(self): |
4529 | + return self.__getattr__('plugin') |
4530 | + |
4531 | + @property |
4532 | + def command(self): |
4533 | + try: |
4534 | + return self.__getattr__('command') |
4535 | + except AttributeError: |
4536 | + return None |
4537 | + |
4538 | + @property |
4539 | + def environ(self): |
4540 | + try: |
4541 | + return self.__getattr__('environ') |
4542 | + except AttributeError: |
4543 | + return None |
4544 | + |
4545 | + @property |
4546 | + def user(self): |
4547 | + try: |
4548 | + return self.__getattr__('user') |
4549 | + except AttributeError: |
4550 | + return None |
4551 | + |
4552 | + def __init__(self, data): |
4553 | + self._data = data |
4554 | + |
4555 | + def __getattr__(self, attr): |
4556 | + if attr in self._data: |
4557 | + return self._data[attr] |
4558 | + raise AttributeError(attr) |
4559 | + |
4560 | + def get_checksum(self): |
4561 | + """ |
4562 | + Compute a checksum of the job definition. |
4563 | + |
4564 | + This method can be used to compute the checksum of the canonical form |
4565 | + of the job definition. The canonical form is the UTF-8 encoded JSON |
4566 | + serialization of the data that makes up the full definition of the job |
4567 | + (all keys and values). The JSON serialization uses no indent and |
4568 | + minimal separators. |
4569 | + |
4570 | + The checksum is defined as the SHA256 hash of the canonical form. |
4571 | + """ |
4572 | + # Ideally we'd use simplejson.dumps() with sorted keys to get |
4573 | + # predictable serialization but that's another dependency. To get |
4574 | + # something simple that is equally reliable, just sort all the keys |
4575 | + # manually and ask standard json to serialize that.. |
4576 | + sorted_data = collections.OrderedDict(sorted(self._data.items())) |
4577 | + # Compute the canonical form which is arbitrarily defined as sorted |
4578 | + # json text with default indent and separator settings. |
4579 | + canonical_form = json.dumps( |
4580 | + sorted_data, indent=None, separators=(',', ':')) |
4581 | + # Compute the sha256 hash of the UTF-8 encoding of the canonical form |
4582 | + # and return the hex digest as the checksum that can be displayed. |
4583 | + return hashlib.sha256(canonical_form.encode('UTF-8')).hexdigest() |
4584 | + |
4585 | + def get_environ_settings(self): |
4586 | + """ |
4587 | + Return a set of requested environment variables |
4588 | + """ |
4589 | + if self.environ is not None: |
4590 | + return {variable for variable in re.split('[\s,]+', self.environ)} |
4591 | + else: |
4592 | + return set() |
4593 | + |
4594 | + def modify_execution_environment(self, environ, packages): |
4595 | + """ |
4596 | + Compute the environment the script will be executed in |
4597 | + """ |
4598 | + # Get a proper environment |
4599 | + env = dict(os.environ) |
4600 | + # Use non-internationalized environment |
4601 | + env['LANG'] = 'C.UTF-8' |
4602 | + # Create CHECKBOX*_SHARE for every checkbox related packages |
4603 | + # Add their respective script directory to the PATH variable |
4604 | + # giving precedence to those located in /usr/lib/ |
4605 | + for path in packages: |
4606 | + basename = os.path.basename(path) |
4607 | + env[basename.upper().replace('-', '_') + '_SHARE'] = path |
4608 | + # Update PATH so that scripts can be found |
4609 | + env['PATH'] = os.pathsep.join([ |
4610 | + os.path.join('usr', 'lib', basename, 'bin'), |
4611 | + os.path.join(path, 'scripts')] |
4612 | + + env.get("PATH", "").split(os.pathsep)) |
4613 | + if 'CHECKBOX_DATA' in env: |
4614 | + env['CHECKBOX_DATA'] = environ['CHECKBOX_DATA'] |
4615 | + # Add new environment variables only if they are defined in the |
4616 | + # job environ property |
4617 | + for key in self.get_environ_settings(): |
4618 | + if key in environ: |
4619 | + env[key] = environ[key] |
4620 | + return env |
4621 | + |
4622 | + |
4623 | +class BaseRFC822Record: |
4624 | + """ |
4625 | + Base class for tracking RFC822 records |
4626 | + |
4627 | + This is a simple container for the dictionary of data. |
4628 | + """ |
4629 | + |
4630 | + def __init__(self, data): |
4631 | + self._data = data |
4632 | + |
4633 | + @property |
4634 | + def data(self): |
4635 | + """ |
4636 | + The data set (dictionary) |
4637 | + """ |
4638 | + return self._data |
4639 | + |
4640 | + |
4641 | +class RFC822SyntaxError(SyntaxError): |
4642 | + """ |
4643 | + SyntaxError subclass for RFC822 parsing functions |
4644 | + """ |
4645 | + |
4646 | + def __init__(self, filename, lineno, msg): |
4647 | + self.filename = filename |
4648 | + self.lineno = lineno |
4649 | + self.msg = msg |
4650 | + |
4651 | + |
4652 | +def load_rfc822_records(stream, data_cls=dict): |
4653 | + """ |
4654 | + Load a sequence of rfc822-like records from a text stream. |
4655 | + |
4656 | + Each record consists of any number of key-value pairs. Subsequent records |
4657 | + are separated by one blank line. A record key may have a multi-line value |
4658 | + if the line starts with whitespace character. |
4659 | + |
4660 | + Returns a list of subsequent values as instances BaseRFC822Record class. If |
4661 | + the optional data_cls argument is collections.OrderedDict then the values |
4662 | + retain their original ordering. |
4663 | + """ |
4664 | + return list(gen_rfc822_records(stream, data_cls)) |
4665 | + |
4666 | + |
4667 | +def gen_rfc822_records(stream, data_cls=dict): |
4668 | + """ |
4669 | + Load a sequence of rfc822-like records from a text stream. |
4670 | + |
4671 | + Each record consists of any number of key-value pairs. Subsequent records |
4672 | + are separated by one blank line. A record key may have a multi-line value |
4673 | + if the line starts with whitespace character. |
4674 | + |
4675 | + Returns a list of subsequent values as instances BaseRFC822Record class. If |
4676 | + the optional data_cls argument is collections.OrderedDict then the values |
4677 | + retain their original ordering. |
4678 | + """ |
4679 | + record = None |
4680 | + data = None |
4681 | + key = None |
4682 | + value_list = None |
4683 | + |
4684 | + def _syntax_error(msg): |
4685 | + """ |
4686 | + Report a syntax error in the current line |
4687 | + """ |
4688 | + try: |
4689 | + filename = stream.name |
4690 | + except AttributeError: |
4691 | + filename = None |
4692 | + return RFC822SyntaxError(filename, lineno, msg) |
4693 | + |
4694 | + def _new_record(): |
4695 | + """ |
4696 | + Reset local state to track new record |
4697 | + """ |
4698 | + nonlocal key |
4699 | + nonlocal value_list |
4700 | + nonlocal record |
4701 | + nonlocal data |
4702 | + key = None |
4703 | + value_list = None |
4704 | + data = data_cls() |
4705 | + record = BaseRFC822Record(data) |
4706 | + |
4707 | + def _commit_key_value_if_needed(): |
4708 | + """ |
4709 | + Finalize the most recently seen key: value pair |
4710 | + """ |
4711 | + nonlocal key |
4712 | + if key is not None: |
4713 | + data[key] = cleandoc('\n'.join(value_list)) |
4714 | + key = None |
4715 | + |
4716 | + # Start with an empty record |
4717 | + _new_record() |
4718 | + # Iterate over subsequent lines of the stream |
4719 | + for lineno, line in enumerate(stream, start=1): |
4720 | + # Treat empty lines as record separators |
4721 | + if line.strip() == "": |
4722 | + # Commit the current record so that the multi-line value of the |
4723 | + # last key, if any, is saved as a string |
4724 | + _commit_key_value_if_needed() |
4725 | + # If data is non-empty, yield the record, this allows us to safely |
4726 | + # use newlines for formatting |
4727 | + if data: |
4728 | + yield record |
4729 | + # Reset local state so that we can build a new record |
4730 | + _new_record() |
4731 | + # Treat lines staring with whitespace as multi-line continuation of the |
4732 | + # most recently seen key-value |
4733 | + elif line.startswith(" "): |
4734 | + if key is None: |
4735 | + # If we have not seen any keys yet then this is a syntax error |
4736 | + raise _syntax_error("Unexpected multi-line value") |
4737 | + # Append the current line to the list of values of the most recent |
4738 | + # key. This prevents quadratic complexity of string concatenation |
4739 | + if line == " .\n": |
4740 | + value_list.append(" ") |
4741 | + elif line == " ..\n": |
4742 | + value_list.append(" .") |
4743 | + else: |
4744 | + value_list.append(line.rstrip()) |
4745 | + # Treat lines with a colon as new key-value pairs |
4746 | + elif ":" in line: |
4747 | + # Since we have a new, key-value pair we need to commit any |
4748 | + # previous key that we may have (regardless of multi-line or |
4749 | + # single-line values). |
4750 | + _commit_key_value_if_needed() |
4751 | + # Parse the line by splitting on the colon, get rid of additional |
4752 | + # whitespace from both key and the value |
4753 | + key, value = line.split(":", 1) |
4754 | + key = key.strip() |
4755 | + value = value.strip() |
4756 | + # Check if the key already exist in this message |
4757 | + if key in record.data: |
4758 | + raise _syntax_error(( |
4759 | + "Job has a duplicate key {!r} " |
4760 | + "with old value {!r} and new value {!r}").format( |
4761 | + key, record.data[key], value)) |
4762 | + # Construct initial value list out of the (only) value that we have |
4763 | + # so far. Additional multi-line values will just append to |
4764 | + # value_list |
4765 | + value_list = [value] |
4766 | + # Treat all other lines as syntax errors |
4767 | + else: |
4768 | + raise _syntax_error("Unexpected non-empty line") |
4769 | + # Make sure to commit the last key from the record |
4770 | + _commit_key_value_if_needed() |
4771 | + # Once we've seen the whole file return the last record, if any |
4772 | + if data: |
4773 | + yield record |
4774 | + |
4775 | + |
4776 | +class Runner: |
4777 | + """ |
4778 | + Runner for jobs |
4779 | + |
4780 | + Executes the command process and pipes back stdout/stderr |
4781 | + """ |
4782 | + |
4783 | + CHECKBOXES = "/usr/share/checkbox*" |
4784 | + |
4785 | + def __init__(self, builtin_jobs=[], packages=[]): |
4786 | + # List of all available jobs in system-wide locations |
4787 | + self.builtin_jobs = builtin_jobs |
4788 | + # List of all checkbox variants, like checkbox-oem(-.*)? |
4789 | + self.packages = packages |
4790 | + |
4791 | + def path_expand(self, path): |
4792 | + for p in glob.glob(path): |
4793 | + self.packages.append(p) |
4794 | + for dirpath, dirs, filenames in os.walk(os.path.join(p, 'jobs')): |
4795 | + for name in filenames: |
4796 | + if name.endswith(".txt"): |
4797 | + yield os.path.join(dirpath, name) |
4798 | + |
4799 | + def main(self, argv=None): |
4800 | + parser = argparse.ArgumentParser(prog="checkbox-trusted-launcher") |
4801 | + group = parser.add_mutually_exclusive_group(required=True) |
4802 | + group.add_argument('--hash', metavar='HASH', help='job hash to match') |
4803 | + group.add_argument( |
4804 | + '--warmup', |
4805 | + action='store_true', |
4806 | + help='Return immediately, only useful when used with pkexec(1)') |
4807 | + parser.add_argument( |
4808 | + '--via', |
4809 | + metavar='LOCAL-JOB-HASH', |
4810 | + dest='via_hash', |
4811 | + help='Local job hash to use to match the generated job') |
4812 | + parser.add_argument( |
4813 | + 'ENV', metavar='NAME=VALUE', nargs='*', |
4814 | + help='Set each NAME to VALUE in the string environment') |
4815 | + args = parser.parse_args(argv) |
4816 | + |
4817 | + if args.warmup: |
4818 | + return 0 |
4819 | + |
4820 | + for filename in self.path_expand(self.CHECKBOXES): |
4821 | + stream = open(filename, "r", encoding="utf-8") |
4822 | + for message in load_rfc822_records(stream): |
4823 | + self.builtin_jobs.append(BaseJob(message.data)) |
4824 | + stream.close() |
4825 | + lookup_list = [j for j in self.builtin_jobs if j.user] |
4826 | + |
4827 | + args.ENV = dict(item.split('=') for item in args.ENV) |
4828 | + |
4829 | + if args.via_hash is not None: |
4830 | + local_list = [j for j in self.builtin_jobs if j.plugin == 'local'] |
4831 | + desired_job_list = [j for j in local_list |
4832 | + if j.get_checksum() == args.via_hash] |
4833 | + if desired_job_list: |
4834 | + via_job = desired_job_list.pop() |
4835 | + via_job_result = subprocess.Popen( |
4836 | + via_job.command, |
4837 | + shell=True, |
4838 | + universal_newlines=True, |
4839 | + stdout=subprocess.PIPE, |
4840 | + env=via_job.modify_execution_environment( |
4841 | + args.ENV, |
4842 | + self.packages) |
4843 | + ) |
4844 | + try: |
4845 | + for message in load_rfc822_records(via_job_result.stdout): |
4846 | + lookup_list.append(BaseJob(message.data)) |
4847 | + finally: |
4848 | + # Always call Popen.wait() in order to avoid zombies |
4849 | + via_job_result.stdout.close() |
4850 | + via_job_result.wait() |
4851 | + |
4852 | + try: |
4853 | + target_job = [j for j in lookup_list |
4854 | + if j.get_checksum() == args.hash][0] |
4855 | + except IndexError: |
4856 | + return "Job not found" |
4857 | + try: |
4858 | + os.execve( |
4859 | + '/bin/bash', |
4860 | + ['bash', '-c', target_job.command], |
4861 | + target_job.modify_execution_environment( |
4862 | + args.ENV, |
4863 | + self.packages) |
4864 | + ) |
4865 | + # if execve doesn't fail, it never returns... |
4866 | + except OSError: |
4867 | + return "Fatal error" |
4868 | + finally: |
4869 | + return "Fatal error" |
4870 | + |
4871 | + |
4872 | +def main(argv=None): |
4873 | + """ |
4874 | + Entry point for the checkbox trusted launcher |
4875 | + """ |
4876 | + runner = Runner() |
4877 | + raise SystemExit(runner.main(argv)) |
4878 | |
4879 | === added file 'plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py.OTHER' |
4880 | --- plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py.OTHER 1970-01-01 00:00:00 +0000 |
4881 | +++ plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py.OTHER 2013-08-28 12:37:27 +0000 |
4882 | @@ -0,0 +1,280 @@ |
4883 | +# This file is part of Checkbox. |
4884 | +# |
4885 | +# Copyright 2013 Canonical Ltd. |
4886 | +# Written by: |
4887 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
4888 | +# |
4889 | +# Checkbox is free software: you can redistribute it and/or modify |
4890 | +# it under the terms of the GNU General Public License as published by |
4891 | +# the Free Software Foundation, either version 3 of the License, or |
4892 | +# (at your option) any later version. |
4893 | +# |
4894 | +# Checkbox is distributed in the hope that it will be useful, |
4895 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4896 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4897 | +# GNU General Public License for more details. |
4898 | +# |
4899 | +# You should have received a copy of the GNU General Public License |
4900 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
4901 | + |
4902 | +""" |
4903 | +plainbox.impl.secure.test_checkbox_trusted_launcher |
4904 | +=================================================== |
4905 | + |
4906 | +Test definitions for plainbox.impl.secure.checkbox_trusted_launcher module |
4907 | +""" |
4908 | + |
4909 | +import os |
4910 | + |
4911 | +from inspect import cleandoc |
4912 | +from io import StringIO |
4913 | +from mock import Mock, patch |
4914 | +from tempfile import NamedTemporaryFile, TemporaryDirectory |
4915 | +from unittest import TestCase |
4916 | + |
4917 | +from plainbox.impl.secure.checkbox_trusted_launcher import BaseJob |
4918 | +from plainbox.impl.secure.checkbox_trusted_launcher import load_rfc822_records |
4919 | +from plainbox.impl.secure.checkbox_trusted_launcher import main |
4920 | +from plainbox.impl.secure.checkbox_trusted_launcher import Runner |
4921 | +from plainbox.impl.test_rfc822 import RFC822ParserTestsMixIn |
4922 | +from plainbox.testing_utils.io import TestIO |
4923 | +from plainbox.testing_utils.testcases import TestCaseWithParameters |
4924 | + |
4925 | + |
4926 | +class TestJobDefinition(TestCase): |
4927 | + |
4928 | + def setUp(self): |
4929 | + self._full_record = { |
4930 | + 'plugin': 'plugin', |
4931 | + 'command': 'command', |
4932 | + 'environ': 'environ', |
4933 | + 'user': 'user' |
4934 | + } |
4935 | + self._min_record = { |
4936 | + 'plugin': 'plugin', |
4937 | + 'name': 'name', |
4938 | + } |
4939 | + |
4940 | + def test_smoke_full_record(self): |
4941 | + job = BaseJob(self._full_record) |
4942 | + self.assertEqual(job.plugin, "plugin") |
4943 | + self.assertEqual(job.command, "command") |
4944 | + self.assertEqual(job.environ, "environ") |
4945 | + self.assertEqual(job.user, "user") |
4946 | + |
4947 | + def test_smoke_min_record(self): |
4948 | + job = BaseJob(self._min_record) |
4949 | + self.assertEqual(job.plugin, "plugin") |
4950 | + self.assertEqual(job.command, None) |
4951 | + self.assertEqual(job.environ, None) |
4952 | + self.assertEqual(job.user, None) |
4953 | + |
4954 | + def test_checksum_smoke(self): |
4955 | + job1 = BaseJob({'plugin': 'plugin', 'user': 'root'}) |
4956 | + identical_to_job1 = BaseJob({'plugin': 'plugin', 'user': 'root'}) |
4957 | + # Two distinct but identical jobs have the same checksum |
4958 | + self.assertEqual(job1.get_checksum(), identical_to_job1.get_checksum()) |
4959 | + job2 = BaseJob({'plugin': 'plugin', 'user': 'anonymous'}) |
4960 | + # Two jobs with different definitions have different checksum |
4961 | + self.assertNotEqual(job1.get_checksum(), job2.get_checksum()) |
4962 | + # The checksum is stable and does not change over time |
4963 | + self.assertEqual( |
4964 | + job1.get_checksum(), |
4965 | + "c47cc3719061e4df0010d061e6f20d3d046071fd467d02d093a03068d2f33400") |
4966 | + |
4967 | + |
4968 | +class ParsingTests(TestCaseWithParameters): |
4969 | + |
4970 | + parameter_names = ('glue',) |
4971 | + parameter_values = ( |
4972 | + ('commas',), |
4973 | + ('spaces',), |
4974 | + ('tabs',), |
4975 | + ('newlines',), |
4976 | + ('spaces_and_commas',), |
4977 | + ('multiple_spaces',), |
4978 | + ('multiple_commas',) |
4979 | + ) |
4980 | + parameters_keymap = { |
4981 | + 'commas': ',', |
4982 | + 'spaces': ' ', |
4983 | + 'tabs': '\t', |
4984 | + 'newlines': '\n', |
4985 | + 'spaces_and_commas': ', ', |
4986 | + 'multiple_spaces': ' ', |
4987 | + 'multiple_commas': ',,,,' |
4988 | + } |
4989 | + |
4990 | + def test_environ_parsing_with_various_separators(self): |
4991 | + job = BaseJob({ |
4992 | + 'name': 'name', |
4993 | + 'plugin': 'plugin', |
4994 | + 'environ': self.parameters_keymap[ |
4995 | + self.parameters.glue].join(['foo', 'bar', 'froz'])}) |
4996 | + expected = set({'foo', 'bar', 'froz'}) |
4997 | + observed = job.get_environ_settings() |
4998 | + self.assertEqual(expected, observed) |
4999 | + |
5000 | + def test_environ_parsing_empty(self): |